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

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

26,564 lines 978.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package Freemius
4 * @copyright Copyright (c) 2015, Freemius, Inc.
5 * @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License Version 3
6 * @since 1.0.3
7 */
8 if ( ! defined( 'ABSPATH' ) ) {
9 exit;
10 }
11
12 // "final class"
13 class Freemius extends Freemius_Abstract {
14 /**
15 * SDK Version
16 *
17 * @var string
18 */
19 public $version = WP_FS__SDK_VERSION;
20
21 #region Plugin Info
22
23 /**
24 * @since 1.0.1
25 *
26 * @var string
27 */
28 private $_slug;
29
30 /**
31 * @since 1.0.0
32 *
33 * @var string
34 */
35 private $_plugin_basename;
36 /**
37 * @since 2.2.1
38 *
39 * @var string
40 */
41 private $_premium_plugin_basename;
42 /**
43 * @since 1.0.0
44 *
45 * @var string
46 */
47 private $_free_plugin_basename;
48 /**
49 * @since 1.0.0
50 *
51 * @var string
52 */
53 private $_plugin_dir_path;
54 /**
55 * @since 1.0.0
56 *
57 * @var string
58 */
59 private $_plugin_dir_name;
60 /**
61 * @since 1.0.0
62 *
63 * @var string
64 */
65 private $_plugin_main_file_path;
66 /**
67 * @var string[]
68 */
69 private $_plugin_data;
70 /**
71 * @since 1.0.9
72 *
73 * @var string
74 */
75 private $_plugin_name;
76 /**
77 * @since 1.2.2
78 *
79 * @var string
80 */
81 private $_module_type;
82
83 #endregion Plugin Info
84
85 /**
86 * @since 1.0.9
87 *
88 * @var bool If false, don't turn Freemius on.
89 */
90 private $_is_on;
91
92 /**
93 * @since 1.1.3
94 *
95 * @var bool If false, don't turn Freemius on.
96 */
97 private $_is_anonymous;
98
99 /**
100 * @since 1.0.9
101 * @var bool If false, issues with connectivity to Freemius API.
102 */
103 private $_has_api_connection;
104
105 /**
106 * @since 1.0.9
107 * @since 2.0.0 Default to true since we need the property during the instance construction, prior to the dynamic_init() execution.
108 * @var bool Hints the SDK if plugin can support anonymous mode (if skip connect is visible).
109 */
110 private $_enable_anonymous = true;
111
112 /**
113 * @since 1.1.7.5
114 * @var bool Hints the SDK if plugin should run in anonymous mode (only adds feedback form).
115 */
116 private $_anonymous_mode;
117
118 /**
119 * @since 1.1.9
120 * @var bool Hints the SDK if plugin have any free plans.
121 */
122 private $_is_premium_only;
123
124 /**
125 * @since 1.2.1.6
126 * @var bool Hints the SDK if plugin have premium code version at all.
127 */
128 private $_has_premium_version;
129
130 /**
131 * @since 1.2.1.6
132 * @var bool Hints the SDK if plugin should ignore pending mode by simulating a skip.
133 */
134 private $_ignore_pending_mode;
135
136 /**
137 * @since 1.0.8
138 * @var bool Hints the SDK if the plugin has any paid plans.
139 */
140 private $_has_paid_plans;
141
142 /**
143 * @since 1.2.1.5
144 * @var int Hints the SDK if the plugin offers a trial period. If negative, no trial, if zero - has a trial but
145 * without a specified period, if positive - the number of trial days.
146 */
147 private $_trial_days = - 1;
148
149 /**
150 * @since 1.2.1.5
151 * @var bool Hints the SDK if the trial requires a payment method or not.
152 */
153 private $_is_trial_require_payment = false;
154
155 /**
156 * @since 1.0.7
157 * @var bool Hints the SDK if the plugin is WordPress.org compliant.
158 */
159 private $_is_org_compliant;
160
161 /**
162 * @since 1.0.7
163 * @var bool Hints the SDK if the plugin is has add-ons.
164 */
165 private $_has_addons;
166
167 /**
168 * @since 2.4.5
169 * @var string Navigation type: 'menu' or 'tabs'.
170 */
171 private $_navigation;
172
173 const NAVIGATION_MENU = 'menu';
174 const NAVIGATION_TABS = 'tabs';
175
176 /**
177 * @since 1.1.6
178 * @var string[]bool.
179 */
180 private $_permissions;
181
182 /**
183 * @var FS_Storage
184 */
185 private $_storage;
186
187 /**
188 * @since 1.2.2.7
189 * @var FS_Cache_Manager
190 */
191 private $_cache;
192
193 /**
194 * @since 1.0.0
195 *
196 * @var FS_Logger
197 */
198 private $_logger;
199 /**
200 * @since 1.0.4
201 *
202 * @var FS_Plugin
203 */
204 private $_plugin = false;
205 /**
206 * @since 1.0.4
207 *
208 * @var FS_Plugin|false
209 */
210 private $_parent_plugin = false;
211 /**
212 * @since 1.1.1
213 *
214 * @var Freemius
215 */
216 private $_parent = false;
217 /**
218 * @since 1.0.1
219 *
220 * @var FS_User
221 */
222 private $_user = false;
223 /**
224 * @since 1.0.1
225 *
226 * @var FS_Site
227 */
228 private $_site = false;
229 /**
230 * @since 1.0.1
231 *
232 * @var FS_Plugin_License
233 */
234 private $_license;
235 /**
236 * @since 1.0.2
237 *
238 * @var FS_Plugin_Plan[]
239 */
240 private $_plans = false;
241 /**
242 * @var FS_Plugin_License[]
243 * @since 1.0.5
244 */
245 private $_licenses = false;
246
247 /**
248 * @since 1.0.1
249 *
250 * @var FS_Admin_Menu_Manager
251 */
252 private $_menu;
253
254 /**
255 * @var FS_Admin_Notices
256 */
257 private $_admin_notices;
258
259 /**
260 * @since 1.1.6
261 *
262 * @var FS_Admin_Notices
263 */
264 private static $_global_admin_notices;
265
266 /**
267 * @var FS_Logger
268 * @since 1.0.0
269 */
270 private static $_static_logger;
271
272 /**
273 * @var FS_Options
274 * @since 1.0.2
275 */
276 private static $_accounts;
277
278 /**
279 * @since 1.2.2
280 *
281 * @var number
282 */
283 private $_module_id;
284
285 /**
286 * @var Freemius[]
287 */
288 private static $_instances = array();
289
290 /**
291 * @since 1.2.3
292 *
293 * @var FS_Affiliate
294 */
295 private $affiliate = null;
296
297 /**
298 * @since 1.2.3
299 *
300 * @var FS_AffiliateTerms
301 */
302 private $plugin_affiliate_terms = null;
303
304 /**
305 * @since 1.2.3
306 *
307 * @var FS_AffiliateTerms
308 */
309 private $custom_affiliate_terms = null;
310
311 /**
312 * @since 2.0.0
313 *
314 * @var bool
315 */
316 private $_is_multisite_integrated;
317
318 /**
319 * @since 2.0.0
320 *
321 * @var bool True if the current request is for a network admin screen and the plugin is network active.
322 */
323 private $_is_network_active;
324
325 /**
326 * @since 2.0.0
327 *
328 * @var int|null The original blog ID the plugin was loaded with.
329 */
330 private $_blog_id = null;
331
332 /**
333 * @since 2.0.0
334 *
335 * @var int|null The current execution context. When true, run on network context. When int, run on the specified blog context.
336 */
337 private $_context_is_network_or_blog_id = null;
338
339 /**
340 * @since 2.0.0
341 *
342 * @var string
343 */
344 private $_dynamically_added_top_level_page_hook_name = '';
345
346 /**
347 * @author Leo Fajardo (@leorw)
348 * @since 2.3.1
349 *
350 * @var bool
351 */
352 private $is_whitelabeled;
353
354 /**
355 * @author Leo Fajardo (@leorw)
356 * @since 2.4.0
357 *
358 * @var bool
359 */
360 private $_is_bundle_license_auto_activation_enabled = false;
361
362 #region Uninstall Reasons IDs
363
364 const REASON_NO_LONGER_NEEDED = 1;
365 const REASON_FOUND_A_BETTER_PLUGIN = 2;
366 const REASON_NEEDED_FOR_A_SHORT_PERIOD = 3;
367 const REASON_BROKE_MY_SITE = 4;
368 const REASON_SUDDENLY_STOPPED_WORKING = 5;
369 const REASON_CANT_PAY_ANYMORE = 6;
370 const REASON_OTHER = 7;
371 const REASON_DIDNT_WORK = 8;
372 const REASON_DONT_LIKE_TO_SHARE_MY_INFORMATION = 9;
373 const REASON_COULDNT_MAKE_IT_WORK = 10;
374 const REASON_GREAT_BUT_NEED_SPECIFIC_FEATURE = 11;
375 const REASON_NOT_WORKING = 12;
376 const REASON_NOT_WHAT_I_WAS_LOOKING_FOR = 13;
377 const REASON_DIDNT_WORK_AS_EXPECTED = 14;
378 const REASON_TEMPORARY_DEACTIVATION = 15;
379
380 #endregion
381
382 /**
383 * @author Leo Fajardo (@leorw)
384 * @since 2.3.1
385 *
386 * @var boolean|null
387 */
388 private $_use_external_pricing = null;
389 /**
390 * @author Leo Fajardo (@leorw)
391 * @since 2.4.2
392 *
393 * @var string|null
394 */
395 private $_pricing_js_path = null;
396
397 const VERSION_MAX_CHARS = 16;
398 const LANGUAGE_MAX_CHARS = 8;
399
400 /* Ctor
401 ------------------------------------------------------------------------------------------------------------------*/
402
403 /**
404 * Main singleton instance.
405 *
406 * @author Vova Feldman (@svovaf)
407 * @since 1.0.0
408 *
409 * @param number $module_id
410 * @param string|bool $slug
411 * @param bool $is_init Since 1.2.1 Is initiation sequence.
412 */
413 private function __construct( $module_id, $slug = false, $is_init = false ) {
414 $main_file = false;
415
416 if ( $is_init && is_numeric( $module_id ) && is_string( $slug ) ) {
417 $main_file = $this->store_id_slug_type_path_map( $module_id, $slug );
418 }
419
420 $this->_module_id = $module_id;
421 $this->_slug = $this->get_slug();
422 $this->_module_type = $this->get_module_type();
423
424 $this->_blog_id = is_multisite() ? get_current_blog_id() : null;
425
426 $this->_storage = FS_Storage::instance( $this->_module_type, $this->_slug );
427
428 $this->_cache = FS_Cache_Manager::get_manager( WP_FS___OPTION_PREFIX . "cache_{$module_id}" );
429
430 $this->_logger = FS_Logger::get_logger( WP_FS__SLUG . '_' . $this->get_unique_affix(), WP_FS__DEBUG_SDK, WP_FS__ECHO_DEBUG_SDK );
431
432 $this->_plugin_main_file_path = $this->_find_caller_plugin_file( $is_init, $main_file );
433 $this->_plugin_dir_path = plugin_dir_path( $this->_plugin_main_file_path );
434 $this->_plugin_basename = $this->get_plugin_basename();
435 $this->_free_plugin_basename = str_replace( '-premium/', '/', $this->_plugin_basename );
436
437 $this->_is_multisite_integrated = (
438 defined( "WP_FS__PRODUCT_{$module_id}_MULTISITE" ) &&
439 ( true === constant( "WP_FS__PRODUCT_{$module_id}_MULTISITE" ) )
440 );
441
442 $this->_is_network_active = (
443 is_multisite() &&
444 $this->_is_multisite_integrated &&
445 // Themes are always network activated, but the ACTUAL activation is per site.
446 $this->is_plugin() &&
447 (
448 is_plugin_active_for_network( $this->_plugin_basename ) ||
449 // Plugin network level activation or uninstall.
450 ( fs_is_network_admin() && is_plugin_inactive( $this->_plugin_basename ) )
451 )
452 );
453
454 $this->_storage->set_network_active(
455 $this->_is_network_active,
456 $this->is_delegated_connection()
457 );
458
459 if ( ! isset( $this->_storage->is_network_activated ) ) {
460 $this->_storage->is_network_activated = $this->_is_network_active;
461 }
462
463 if ( $this->_storage->is_network_activated != $this->_is_network_active ) {
464 // Update last activation level.
465 $this->_storage->is_network_activated = $this->_is_network_active;
466
467 $this->maybe_adjust_storage();
468 }
469
470 #region Migration
471
472 if ( is_multisite() ) {
473 /**
474 * If the install_timestamp exists on the site level but doesn't exist on the
475 * network level storage, it means that we need to process the storage with migration.
476 *
477 * 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.
478 *
479 * @author Vova Feldman (@svovaf)
480 * @since 2.0.0
481 */
482 if ( false === $this->_storage->get( 'install_timestamp', false, true ) &&
483 false !== $this->_storage->get( 'install_timestamp', false, false )
484 ) {
485 // Initiate storage migration.
486 $this->_storage->migrate_to_network();
487
488 // Migrate module cache to network level storage.
489 $this->_cache->migrate_to_network();
490 }
491 }
492
493 #endregion
494
495 $base_name_split = explode( '/', $this->_plugin_basename );
496 $this->_plugin_dir_name = $base_name_split[0];
497
498 if ( $this->_logger->is_on() ) {
499 $this->_logger->info( 'plugin_main_file_path = ' . $this->_plugin_main_file_path );
500 $this->_logger->info( 'plugin_dir_path = ' . $this->_plugin_dir_path );
501 $this->_logger->info( 'plugin_basename = ' . $this->_plugin_basename );
502 $this->_logger->info( 'free_plugin_basename = ' . $this->_free_plugin_basename );
503 $this->_logger->info( 'plugin_dir_name = ' . $this->_plugin_dir_name );
504 }
505
506 // Remember link between file to slug.
507 $this->store_file_slug_map();
508
509 // Store plugin's initial install timestamp.
510 if ( ! isset( $this->_storage->install_timestamp ) ) {
511 $this->_storage->install_timestamp = WP_FS__SCRIPT_START_TIME;
512 }
513
514 if ( ! is_object( $this->_plugin ) ) {
515 $this->_plugin = FS_Plugin_Manager::instance( $this->_module_id )->get();
516 }
517
518 $this->_admin_notices = FS_Admin_Notices::instance(
519 $this->_slug . ( $this->is_theme() ? ':theme' : '' ),
520 /**
521 * Ensure that the admin notice will always have a title by using the stored plugin title if available and
522 * retrieving the title via the "get_plugin_name" method if there is no stored plugin title available.
523 *
524 * @author Leo Fajardo (@leorw)
525 * @since 1.2.2
526 */
527 ( is_object( $this->_plugin ) && isset( $this->_plugin->title ) ?
528 $this->_plugin->title :
529 $this->get_plugin_name()
530 ),
531 $this->get_unique_affix()
532 );
533
534 if ( 'true' === fs_request_get( 'fs_clear_api_cache' ) ||
535 fs_request_is_action( 'restart_freemius' )
536 ) {
537 FS_Api::clear_cache();
538 $this->_cache->clear();
539 }
540
541 $this->register_constructor_hooks();
542
543 /**
544 * Starting from version 2.0.0, `FS_Site` entities no longer have the `plan` property and have `plan_id`
545 * instead. This should be called before calling `_load_account()`, otherwise, `$this->_site` will not be
546 * loaded in `_load_account` for versions of SDK starting from 2.0.0.
547 *
548 * @author Leo Fajardo (@leorw)
549 */
550 self::migrate_install_plan_to_plan_id( $this->_storage );
551
552 $this->_load_account();
553
554 $this->_version_updates_handler();
555 }
556
557 /**
558 * @author Leo Fajardo (@leorw)
559 * @since 2.3.0
560 */
561 private function maybe_adjust_storage() {
562 $install_timestamp = null;
563 $prev_is_premium = null;
564
565 $options_to_update = array();
566
567 $is_network_admin = fs_is_network_admin();
568
569 $network_install_timestamp = $this->_storage->get( 'install_timestamp', null, true );
570
571 if ( ! $is_network_admin ) {
572 if ( is_null( $network_install_timestamp ) ) {
573 // Plugin was not network-activated before.
574 return;
575 }
576
577 if ( is_null( $this->_storage->get( 'install_timestamp', null, false ) ) ) {
578 // Set the `install_timestamp` only if it's not yet set.
579 $install_timestamp = $network_install_timestamp;
580 }
581
582 $prev_is_premium = $this->_storage->get( 'prev_is_premium', null, true );
583 } else {
584 $current_wp_user = self::_get_current_wp_user();
585 $current_fs_user = self::_get_user_by_email( $current_wp_user->user_email );
586 $network_user_info = array();
587
588 $skips_count = 0;
589
590 $sites = self::get_sites();
591 $sites_count = count( $sites );
592
593 $blog_id_2_install_map = array();
594
595 $is_first_non_ignored_blog = true;
596
597 foreach ( $sites as $site ) {
598 $blog_id = self::get_site_blog_id( $site );
599
600 $blog_install_timestamp = $this->_storage->get( 'install_timestamp', null, $blog_id );
601
602 if ( is_null( $blog_install_timestamp ) ) {
603 // Plugin has not been installed on this blog.
604 continue;
605 }
606
607 $is_earlier_install = (
608 ! is_null( $install_timestamp ) &&
609 $blog_install_timestamp < $install_timestamp
610 );
611
612 $install = $this->get_install_by_blog_id( $blog_id );
613
614 $update_network_user_info = false;
615
616 if ( ! is_object( $install ) ) {
617 if ( ! $this->_storage->get( 'is_anonymous', false, $blog_id ) ) {
618 // The opt-in decision (whether to skip or opt in) is yet to be made.
619 continue;
620 }
621
622 $skips_count ++;
623 } else {
624 $blog_id_2_install_map[ $blog_id ] = $install;
625
626 if ( empty( $network_user_info ) ) {
627 // Set the network user info for the 1st time. Choose any user information whether or not it is for the current WP user.
628 $update_network_user_info = true;
629 }
630
631 if ( ! $update_network_user_info &&
632 is_object( $current_fs_user ) &&
633 $network_user_info['user_id'] != $current_fs_user->id &&
634 $install->user_id == $current_fs_user->id
635 ) {
636 // If an install that is owned by the current WP user is found, use its user information instead.
637 $update_network_user_info = true;
638 }
639
640 if ( ! $update_network_user_info &&
641 $is_earlier_install &&
642 ( ! is_object( $current_fs_user ) || $current_fs_user->id == $install->user_id )
643 ) {
644 // 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.
645 $update_network_user_info = true;
646 }
647 }
648
649 if ( $update_network_user_info ) {
650 $network_user_info = array(
651 'user_id' => $install->user_id,
652 'blog_id' => $blog_id
653 );
654 }
655
656 $site_prev_is_premium = $this->_storage->get( 'prev_is_premium', null, $blog_id );
657
658 if ( $is_first_non_ignored_blog ) {
659 $prev_is_premium = $site_prev_is_premium;
660
661 if ( is_null( $network_install_timestamp ) ) {
662 $install_timestamp = $blog_install_timestamp;
663 }
664
665 $is_first_non_ignored_blog = false;
666
667 continue;
668 }
669
670 if ( ! is_null( $prev_is_premium ) && $prev_is_premium !== $site_prev_is_premium ) {
671 // If a different `$site_prev_is_premium` value is found, do not include the option in the collection of options to update.
672 $prev_is_premium = null;
673 }
674
675 if ( $is_earlier_install ) {
676 // If an earlier install timestamp is found.
677 $install_timestamp = $blog_install_timestamp;
678 }
679 }
680
681 $installs_count = count( $blog_id_2_install_map );
682
683 if ( $sites_count === ( $installs_count + $skips_count ) ) {
684 if ( ! empty( $network_user_info ) ) {
685 $options_to_update['network_user_id'] = $network_user_info['user_id'];
686 $options_to_update['network_install_blog_id'] = $network_user_info['blog_id'];
687
688 foreach ( $blog_id_2_install_map as $blog_id => $install ) {
689 if ( $install->user_id == $network_user_info['user_id'] ) {
690 continue;
691 }
692
693 $this->_storage->store( 'is_delegated_connection', true, $blog_id );
694 }
695 }
696
697 if ( $sites_count === $skips_count ) {
698 /**
699 * Assume network-level skipping as the intended action if all actions identified were only
700 * skipping of the connection (i.e., no opt-ins and delegated connections so far).
701 */
702 $options_to_update['is_anonymous_ms'] = true;
703 } else if ( $sites_count === $installs_count ) {
704 /**
705 * Assume network-level opt-in as the intended action if all actions identified were only opt-ins
706 * (i.e., no delegation and skipping of the connections so far).
707 */
708 $options_to_update['is_network_connected'] = true;
709 }
710 }
711 }
712
713 if ( ! is_null( $install_timestamp ) ) {
714 $options_to_update['install_timestamp'] = $install_timestamp;
715 }
716
717 if ( ! is_null( $prev_is_premium ) ) {
718 $options_to_update['prev_is_premium'] = $prev_is_premium;
719 }
720
721 if ( ! empty( $options_to_update ) ) {
722 $this->adjust_storage( $options_to_update, $is_network_admin );
723 }
724 }
725
726 /**
727 * @author Leo Fajardo (@leorw)
728 * @since 2.3.0
729 *
730 * @param array $options
731 * @param bool $is_network_admin
732 */
733 private function adjust_storage( $options, $is_network_admin ) {
734 foreach ( $options as $name => $value ) {
735 $this->_storage->store( $name, $value, $is_network_admin ? true : null );
736 }
737 }
738
739 /**
740 * Checks whether this module has a settings menu.
741 *
742 * @author Leo Fajardo (@leorw)
743 * @since 1.2.2
744 *
745 * @return bool
746 */
747 function has_settings_menu() {
748 return ( $this->_is_network_active && fs_is_network_admin() ) ?
749 $this->_menu->has_network_menu() :
750 $this->_menu->has_menu();
751 }
752
753 /**
754 * 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.
755 *
756 * @author Vova Feldman (@svovaf)
757 * @since 2.4.5
758 *
759 * @return bool
760 */
761 function show_opt_in_on_themes_page() {
762 if ( ! $this->is_free_wp_org_theme() ) {
763 return false;
764 }
765
766 if ( ! $this->has_settings_menu() ) {
767 return true;
768 }
769
770 return $this->show_settings_with_tabs();
771 }
772
773 /**
774 * If `true` the opt-in should be shown on the product's main setting page.
775 *
776 * @author Vova Feldman (@svovaf)
777 * @since 2.4.5
778 *
779 * @return bool
780 *
781 * @uses show_opt_in_on_themes_page();
782 */
783 function show_opt_in_on_setting_page() {
784 return ! $this->show_opt_in_on_themes_page();
785 }
786
787 /**
788 * If `true` the settings should be shown using tabs.
789 *
790 * @author Vova Feldman (@svovaf)
791 * @since 2.4.5
792 *
793 * @return bool
794 */
795 function show_settings_with_tabs() {
796 return ( self::NAVIGATION_TABS === $this->_navigation );
797 }
798
799 /**
800 * Check if the context module is free wp.org theme.
801 *
802 * This method is helpful because:
803 * 1. wp.org themes are limited to a single submenu item,
804 * and sub-submenu items are most likely not allowed (never verified).
805 * 2. wp.org themes are not allowed to redirect the user
806 * after the theme activation, therefore, the agreed UX
807 * is showing the opt-in as a modal dialog box after
808 * activation (approved by @otto42, @emiluzelac, @greenshady, @grapplerulrich).
809 *
810 * @author Vova Feldman (@svovaf)
811 * @since 1.2.2.7
812 *
813 * @return bool
814 */
815 function is_free_wp_org_theme() {
816 return (
817 $this->is_theme() &&
818 $this->is_org_repo_compliant() &&
819 ! $this->is_premium()
820 );
821 }
822
823 /**
824 * Checks whether this a submenu item is visible.
825 *
826 * @author Vova Feldman (@svovaf)
827 * @since 1.2.2.6
828 * @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.
829 *
830 * @param string $slug
831 * @param bool $is_tabs_visibility_check This is used to decide if the associated tab should be shown or hidden.
832 *
833 * @return bool
834 */
835 function is_submenu_item_visible( $slug, $is_tabs_visibility_check = false ) {
836 if ( $this->is_admin_page( $slug ) ) {
837 /**
838 * It is the current context page, so show the submenu item
839 * so the user will have the right context page, even if it
840 * was set to hidden.
841 */
842 return true;
843 }
844
845 if ( ! $this->has_settings_menu() ) {
846 // No menu settings at all.
847 return false;
848 }
849
850 if (
851 ! $is_tabs_visibility_check &&
852 $this->is_org_repo_compliant() &&
853 $this->show_settings_with_tabs()
854 ) {
855 /**
856 * wp.org themes are limited to a single submenu item, and
857 * sub-submenu items are most likely not allowed (never verified).
858 */
859 return false;
860 }
861
862 return $this->_menu->is_submenu_item_visible( $slug );
863 }
864
865 /**
866 * Check if a Freemius page should be accessible via the UI.
867 *
868 * @author Vova Feldman (@svovaf)
869 * @since 1.2.2.7
870 *
871 * @param string $slug
872 *
873 * @return bool
874 */
875 function is_page_visible( $slug ) {
876 if ( $this->is_admin_page( $slug ) ) {
877 return true;
878 }
879
880 return $this->_menu->is_submenu_item_visible( $slug, true, true );
881 }
882
883 /**
884 * @author Vova Feldman (@svovaf)
885 * @since 1.0.9
886 */
887 private function _version_updates_handler() {
888 if ( ! isset( $this->_storage->sdk_version ) || $this->_storage->sdk_version != $this->version ) {
889 // Freemius version upgrade mode.
890 $this->_storage->sdk_last_version = $this->_storage->sdk_version;
891 $this->_storage->sdk_version = $this->version;
892
893 if ( empty( $this->_storage->sdk_last_version ) ||
894 version_compare( $this->_storage->sdk_last_version, $this->version, '<' )
895 ) {
896 $this->_storage->sdk_upgrade_mode = true;
897 $this->_storage->sdk_downgrade_mode = false;
898 } else {
899 $this->_storage->sdk_downgrade_mode = true;
900 $this->_storage->sdk_upgrade_mode = false;
901
902 }
903
904 $this->do_action( 'sdk_version_update', $this->_storage->sdk_last_version, $this->version );
905 }
906
907 $plugin_version = $this->get_plugin_version();
908 if ( ! isset( $this->_storage->plugin_version ) || $this->_storage->plugin_version != $plugin_version ) {
909 // Plugin version upgrade mode.
910 $this->_storage->plugin_last_version = $this->_storage->plugin_version;
911 $this->_storage->plugin_version = $plugin_version;
912
913 if ( empty( $this->_storage->plugin_last_version ) ||
914 version_compare( $this->_storage->plugin_last_version, $plugin_version, '<' )
915 ) {
916 $this->_storage->plugin_upgrade_mode = true;
917 $this->_storage->plugin_downgrade_mode = false;
918 } else {
919 $this->_storage->plugin_downgrade_mode = true;
920 $this->_storage->plugin_upgrade_mode = false;
921 }
922
923 if ( ! empty( $this->_storage->plugin_last_version ) ) {
924 // Different version of the plugin was installed before, therefore it's an update.
925 $this->_storage->is_plugin_new_install = false;
926 }
927
928 $this->do_action( 'plugin_version_update', $this->_storage->plugin_last_version, $plugin_version );
929 }
930 }
931
932 #--------------------------------------------------------------------------------
933 #region Data Migration on SDK Update
934 #--------------------------------------------------------------------------------
935
936 /**
937 * @author Vova Feldman (@svovaf)
938 * @since 1.1.5
939 *
940 * @param string $sdk_prev_version
941 * @param string $sdk_version
942 */
943 function _sdk_version_update( $sdk_prev_version, $sdk_version ) {
944 if ( empty( $sdk_prev_version ) ) {
945 return;
946 }
947
948 if (
949 version_compare( $sdk_prev_version, '2.5.1', '<' ) &&
950 version_compare( $sdk_version, '2.5.1', '>=' )
951 ) {
952 if ( $this->is_registered( true ) ) {
953 /**
954 * Migrate to new permissions layer.
955 */
956 require_once WP_FS__DIR_INCLUDES . '/supplements/fs-migration-2.5.1.php';
957
958 $install_by_blog_id = is_multisite() ?
959 $this->get_blog_install_map() :
960 array( 0 => $this->_site );
961
962 fs_migrate_251( $this, $install_by_blog_id );
963 }
964 }
965 }
966
967 /**
968 * @author Leo Fajardo (@leorw)
969 * @since 2.0.0
970 *
971 * @param \FS_Storage $storage
972 * @param bool|int|null $blog_id
973 */
974 private static function migrate_install_plan_to_plan_id( FS_Storage $storage, $blog_id = null ) {
975 if ( empty( $storage->sdk_version ) ) {
976 // New installation of the plugin, no need to upgrade.
977 return;
978 }
979
980 if ( ! version_compare( $storage->sdk_version, '2.0.0', '<' ) ) {
981 // Previous version is >= 2.0.0, so no need to migrate.
982 return;
983 }
984
985 // Alias.
986 $module_type = $storage->get_module_type();
987 $module_slug = $storage->get_module_slug();
988
989 $installs = self::get_all_sites( $module_type, $blog_id );
990 $install = isset( $installs[ $module_slug ] ) ? $installs[ $module_slug ] : null;
991
992 if ( ! is_object( $install ) ) {
993 return;
994 }
995
996 if ( isset( $install->plan ) && is_object( $install->plan ) ) {
997 if ( isset( $install->plan->id ) && ! empty( $install->plan->id ) ) {
998 $install->plan_id = self::_decrypt( $install->plan->id );
999 }
1000
1001 unset( $install->plan );
1002
1003 $installs[ $module_slug ] = clone $install;
1004
1005 self::set_account_option_by_module(
1006 $module_type,
1007 'sites',
1008 $installs,
1009 true,
1010 $blog_id
1011 );
1012 }
1013 }
1014
1015 /**
1016 * @author Vova Feldman (@svovaf)
1017 * @since 1.2.2.7
1018 *
1019 * @param string $plugin_prev_version
1020 * @param string $plugin_version
1021 */
1022 function _after_version_update( $plugin_prev_version, $plugin_version ) {
1023 if ( $this->is_theme() ) {
1024 // Expire the cache of the previous tabs since the theme may
1025 // have setting updates.
1026 $this->_cache->expire( 'tabs' );
1027 $this->_cache->expire( 'tabs_stylesheets' );
1028 }
1029 }
1030
1031 /**
1032 * A special migration logic for the $_accounts, executed for all the plugins in the system:
1033 * - Moves some data to the network level storage.
1034 * - If the plugin's connection was skipped for all sites, set the plugin as if it was network skipped.
1035 * - If the plugin's connection was ignored for all sites, don't do anything in terms of the network connection.
1036 * - 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.
1037 * - 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.
1038 * - 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.
1039 *
1040 * @author Vova Feldman (@svovaf)
1041 * @since 2.0.0
1042 */
1043 private static function migrate_accounts_to_network() {
1044 $sites = self::get_sites();
1045 $sites_count = count( $sites );
1046 $connection_status = array();
1047 $plugin_slugs = array();
1048 foreach ( $sites as $site ) {
1049 $blog_id = self::get_site_blog_id( $site );
1050
1051 self::$_accounts->migrate_to_network( $blog_id );
1052
1053 /**
1054 * Build a list of all Freemius powered plugins slugs.
1055 */
1056 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array(), $blog_id );
1057 foreach ( $id_slug_type_path_map as $module_id => $data ) {
1058 if ( WP_FS__MODULE_TYPE_PLUGIN === $data['type'] ) {
1059 $plugin_slugs[ $data['slug'] ] = true;
1060 }
1061 }
1062
1063 $installs = self::get_account_option( 'sites', WP_FS__MODULE_TYPE_PLUGIN, $blog_id );
1064
1065 if ( is_array( $installs ) ) {
1066 foreach ( $installs as $slug => $install ) {
1067 if ( ! isset( $connection_status[ $slug ] ) ) {
1068 $connection_status[ $slug ] = array();
1069 }
1070
1071 if ( is_object( $install ) &&
1072 FS_Site::is_valid_id( $install->id ) &&
1073 FS_User::is_valid_id( $install->user_id )
1074 ) {
1075 $connection_status[ $slug ][ $blog_id ] = $install->user_id;
1076 }
1077 }
1078 }
1079 }
1080
1081 foreach ( $plugin_slugs as $slug => $true ) {
1082 if ( ! isset( $connection_status[ $slug ] ) ) {
1083 $connection_status[ $slug ] = array();
1084 }
1085
1086 foreach ( $sites as $site ) {
1087 $blog_id = self::get_site_blog_id( $site );
1088
1089 if ( isset( $connection_status[ $slug ][ $blog_id ] ) ) {
1090 continue;
1091 }
1092
1093 $storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
1094
1095 $is_anonymous = $storage->get( 'is_anonymous', null, $blog_id );
1096
1097 if ( ! is_null( $is_anonymous ) ) {
1098 // Since 1.1.3 is_anonymous is an array.
1099 if ( is_array( $is_anonymous ) && isset( $is_anonymous['is'] ) ) {
1100 $is_anonymous = $is_anonymous['is'];
1101 }
1102
1103 if ( is_bool( $is_anonymous ) && true === $is_anonymous ) {
1104 $connection_status[ $slug ][ $blog_id ] = 'skipped';
1105 }
1106 }
1107
1108 if ( ! isset( $connection_status[ $slug ][ $blog_id ] ) ) {
1109 $connection_status[ $slug ][ $blog_id ] = 'ignored';
1110 }
1111 }
1112 }
1113
1114 $super_admins = array();
1115
1116 foreach ( $connection_status as $slug => $blogs_status ) {
1117 $skips = 0;
1118 $ignores = 0;
1119 $connections = 0;
1120 $opted_in_users = array();
1121 $opted_in_super_admins = array();
1122
1123 $storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
1124
1125 foreach ( $blogs_status as $blog_id => $status_or_user_id ) {
1126 if ( 'skipped' === $status_or_user_id ) {
1127 $skips ++;
1128 } else if ( 'ignored' === $status_or_user_id ) {
1129 $ignores ++;
1130 } else if ( FS_User::is_valid_id( $status_or_user_id ) ) {
1131 $connections ++;
1132
1133 if ( ! isset( $opted_in_users[ $status_or_user_id ] ) ) {
1134 $opted_in_users[ $status_or_user_id ] = array();
1135 }
1136
1137 $opted_in_users[ $status_or_user_id ][] = $blog_id;
1138
1139 if ( isset( $super_admins[ $status_or_user_id ] ) ||
1140 self::is_super_admin( $status_or_user_id )
1141 ) {
1142 // Cache super-admin data.
1143 $super_admins[ $status_or_user_id ] = true;
1144
1145 // Remember opted-in super-admins for the plugin.
1146 $opted_in_super_admins[ $status_or_user_id ] = true;
1147 }
1148 }
1149 }
1150
1151 $main_super_admin_user_id = null;
1152 $all_migrated = false;
1153 if ( $sites_count == $skips ) {
1154 // All sites were skipped -> network skip by copying the anonymous mode from any of the sites.
1155 $storage->is_anonymous_ms = $storage->is_anonymous;
1156
1157 $all_migrated = true;
1158 } else if ( $sites_count == $ignores ) {
1159 // Don't do anything, still in activation mode.
1160
1161 $all_migrated = true;
1162 } else if ( 0 < count( $opted_in_super_admins ) ) {
1163 // Find the super-admin with the majority of installs.
1164 $max_installs_by_super_admin = 0;
1165 foreach ( $opted_in_super_admins as $user_id => $true ) {
1166 $installs_count = count( $opted_in_users[ $user_id ] );
1167
1168 if ( $installs_count > $max_installs_by_super_admin ) {
1169 $max_installs_by_super_admin = $installs_count;
1170 $main_super_admin_user_id = $user_id;
1171 }
1172 }
1173
1174 if ( $sites_count == $connections && 1 == count( $opted_in_super_admins ) ) {
1175 // Super-admin opted-in for all sites in the network.
1176 $storage->is_network_connected = true;
1177
1178 $all_migrated = true;
1179 }
1180
1181 // Store network user.
1182 $storage->network_user_id = $main_super_admin_user_id;
1183
1184 $storage->network_install_blog_id = ( $sites_count == $connections ) ?
1185 // Since all sites are opted-in, associating with the main site.
1186 get_current_blog_id() :
1187 // Associating with the 1st found opted-in site.
1188 $opted_in_users[ $main_super_admin_user_id ][0];
1189
1190 /**
1191 * Make sure we migrate the plan ID of the network install, otherwise, if after the migration
1192 * the 1st page that will be loaded is the network level WP Admin and $storage->network_install_blog_id
1193 * is different than the main site of the network, the $this->_site will not be set since the plan_id
1194 * will be empty.
1195 */
1196 $storage->migrate_to_network();
1197 self::migrate_install_plan_to_plan_id( $storage, $storage->network_install_blog_id );
1198 } else {
1199 // At least one opt-in. All the opt-in were created by a non-super-admin.
1200 if ( 0 == $ignores ) {
1201 // All sites were opted-in or skipped, all by non-super-admin. So delegate all.
1202 $storage->store( 'is_delegated_connection', true, true );
1203
1204 $all_migrated = true;
1205 }
1206 }
1207
1208 if ( ! $all_migrated ) {
1209 /**
1210 * Delegate all sites that were:
1211 * 1) Opted-in by a user that is NOT the main-super-admin.
1212 * 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.
1213 */
1214 foreach ( $blogs_status as $blog_id => $status_or_user_id ) {
1215 if ( $status_or_user_id == $main_super_admin_user_id ) {
1216 continue;
1217 }
1218
1219 if ( FS_User::is_valid_id( $status_or_user_id ) ||
1220 ( 'skipped' === $status_or_user_id && is_null( $main_super_admin_user_id ) )
1221 ) {
1222 $storage->store( 'is_delegated_connection', true, $blog_id );
1223 }
1224 }
1225 }
1226
1227
1228 if ( ( $connections + $skips > 0 ) ) {
1229 if ( $ignores > 0 ) {
1230 /**
1231 * If admin already opted-in or skipped in any of the network sites, and also
1232 * have sites which the connection decision was not yet taken, set this plugin
1233 * into network activation mode so the super-admin can choose what to do with
1234 * the rest of the sites.
1235 */
1236 self::set_network_upgrade_mode( $storage );
1237 }
1238 }
1239 }
1240 }
1241
1242 /**
1243 * Set a module into network upgrade mode.
1244 *
1245 * @author Vova Feldman (@svovaf)
1246 * @since 2.0.0
1247 *
1248 * @param \FS_Storage $storage
1249 *
1250 * @return bool
1251 */
1252 private static function set_network_upgrade_mode( FS_Storage $storage ) {
1253 return $storage->is_network_activation = true;
1254 }
1255
1256 /**
1257 * Will return true after upgrading to the SDK with the network level integration,
1258 * when the super-admin involvement is required regarding the rest of the sites.
1259 *
1260 * @author Vova Feldman (@svovaf)
1261 * @since 2.0.0
1262 *
1263 * @return bool
1264 */
1265 function is_network_upgrade_mode() {
1266 return $this->_storage->get( 'is_network_activation' );
1267 }
1268
1269 /**
1270 * Clear flag after the upgrade mode completion.
1271 *
1272 * @author Vova Feldman (@svovaf)
1273 * @since 2.0.0
1274 *
1275 * @return bool True if network activation was on and now completed.
1276 */
1277 private function network_upgrade_mode_completed() {
1278 if ( fs_is_network_admin() && $this->is_network_upgrade_mode() ) {
1279 $this->_storage->remove( 'is_network_activation' );
1280
1281 return true;
1282 }
1283
1284 return false;
1285 }
1286
1287 #endregion
1288
1289 /**
1290 * This action is connected to the 'plugins_loaded' hook and helps to determine
1291 * if this is a new plugin installation or a plugin update.
1292 *
1293 * There are 3 different use-cases:
1294 * 1) New plugin installation right with Freemius:
1295 * 1.1 _activate_plugin_event_hook() will be executed first
1296 * 1.2 Since $this->_storage->is_plugin_new_install is not set,
1297 * and $this->_storage->plugin_last_version is not set,
1298 * $this->_storage->is_plugin_new_install will be set to TRUE.
1299 * 1.3 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install will
1300 * be already set to TRUE.
1301 *
1302 * 2) Plugin update, didn't have Freemius before, and now have the SDK:
1303 * 2.1 _activate_plugin_event_hook() will not be executed, because
1304 * the activation hook do NOT fires on updates since WP 3.1.
1305 * 2.2 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install will
1306 * be empty, therefore, it will be set to FALSE.
1307 *
1308 * 3) Plugin update, had Freemius in prev version as well:
1309 * 3.1 _version_updates_handler() will be executed 1st, since FS was installed
1310 * before, $this->_storage->plugin_last_version will NOT be empty,
1311 * therefore, $this->_storage->is_plugin_new_install will be set to FALSE.
1312 * 3.2 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install is
1313 * already set, therefore, it will not be modified.
1314 *
1315 * Use-case #3 is backward compatible, #3.1 will be executed since 1.0.9.
1316 *
1317 * NOTE:
1318 * The only fallback of this mechanism is if an admin updates a plugin based on use-case #2,
1319 * and then, the next immediate PageView is the plugin's main settings page, it will not
1320 * show the opt-in right away. The reason it will happen is because Freemius execution
1321 * will be turned off till the plugin is fully loaded at least once
1322 * (till $this->_storage->was_plugin_loaded is TRUE).
1323 *
1324 * @author Vova Feldman (@svovaf)
1325 * @since 1.1.9
1326 *
1327 */
1328 function _plugins_loaded() {
1329 // Update flag that plugin was loaded with Freemius at least once.
1330 $this->_storage->was_plugin_loaded = true;
1331
1332 /**
1333 * Bug fix - only set to false when it's a plugin, due to the
1334 * execution sequence of the theme hooks and our methods, if
1335 * this will be set for themes, Freemius will always assume
1336 * it's a theme update.
1337 *
1338 * @author Vova Feldman (@svovaf)
1339 * @since 1.2.2.2
1340 */
1341 if ( $this->is_plugin() &&
1342 ! isset( $this->_storage->is_plugin_new_install )
1343 ) {
1344 $this->_storage->is_plugin_new_install = (
1345 ! is_plugin_active( $this->_plugin_basename ) &&
1346 empty( $this->_storage->plugin_last_version )
1347 );
1348 }
1349 }
1350
1351 /**
1352 * Opens the support forum subemenu item in a new browser page.
1353 *
1354 * @author Vova Feldman (@svovaf)
1355 * @since 2.1.4
1356 */
1357 static function _open_support_forum_in_new_page() {
1358 ?>
1359 <script type="text/javascript">
1360 (function ($) {
1361 $('.fs-submenu-item.wp-support-forum').parent().attr( { target: '_blank', rel: 'noopener noreferrer' } );
1362 })(jQuery);
1363 </script>
1364 <?php
1365 }
1366
1367 /**
1368 * @author Vova Feldman (@svovaf)
1369 * @since 1.0.9
1370 */
1371 private function register_constructor_hooks() {
1372 $this->_logger->entrance();
1373
1374 if ( is_admin() ) {
1375 add_action( 'admin_init', array( &$this, '_hook_action_links_and_register_account_hooks' ) );
1376
1377 if ( $this->is_plugin() ) {
1378 if ( self::is_plugin_install_page() && true !== fs_request_get_bool( 'fs_allow_updater_and_dialog' ) ) {
1379 /**
1380 * Unless the `fs_allow_updater_and_dialog` URL param exists and its value is `true`, make
1381 * Freemius-related updates unavailable on the "Add Plugins" admin page (/plugin-install.php)
1382 * so that they won't interfere with the .org plugins' functionalities on that page (e.g.
1383 * updating of a .org plugin).
1384 */
1385 add_filter( 'site_transient_update_plugins', array( 'Freemius', '_remove_fs_updates_from_plugin_install_page' ), 10, 2 );
1386 } else if ( self::is_plugins_page() || self::is_updates_page() ) {
1387 /**
1388 * 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.
1389 *
1390 * @author Leo Fajardo (@leorw)
1391 * @since 2.2.3
1392 */
1393 add_action( 'admin_footer', array( 'Freemius', '_prepend_fs_allow_updater_and_dialog_flag_url_param' ) );
1394 }
1395
1396 $plugin_dir = dirname( $this->_plugin_dir_path ) . '/';
1397
1398 /**
1399 * @since 1.2.2
1400 *
1401 * Hook to both free and premium version activations to support
1402 * auto deactivation on the other version activation.
1403 */
1404 register_activation_hook(
1405 $plugin_dir . $this->_free_plugin_basename,
1406 array( &$this, '_activate_plugin_event_hook' )
1407 );
1408
1409 register_activation_hook(
1410 $plugin_dir . $this->premium_plugin_basename(),
1411 array( &$this, '_activate_plugin_event_hook' )
1412 );
1413 } else {
1414 add_action( 'after_switch_theme', array( &$this, '_activate_theme_event_hook' ), 10, 2 );
1415
1416 add_action( 'admin_footer', array( &$this, '_style_premium_theme' ) );
1417 }
1418
1419 /**
1420 * Part of the mechanism to identify new plugin install vs. plugin update.
1421 *
1422 * @author Vova Feldman (@svovaf)
1423 * @since 1.1.9
1424 */
1425 if ( empty( $this->_storage->was_plugin_loaded ) ) {
1426 /**
1427 * During the plugin activation (not theme), 'plugins_loaded' will be already executed
1428 * when the logic gets here since the activation logic first add the activate plugins,
1429 * then triggers 'plugins_loaded', and only then include the code of the plugin that
1430 * is activated. Which means that _plugins_loaded() will NOT be executed during the
1431 * plugin activation, and that IS intentional.
1432 *
1433 * @author Vova Feldman (@svovaf)
1434 */
1435 if ( $this->is_plugin() &&
1436 $this->is_activation_mode( false ) &&
1437 0 == did_action( 'plugins_loaded' )
1438 ) {
1439 add_action( 'plugins_loaded', array( &$this, '_plugins_loaded' ) );
1440 } else {
1441 // If was activated before, then it was already loaded before.
1442 $this->_plugins_loaded();
1443 }
1444 }
1445
1446 if ( ! self::is_ajax() ) {
1447 if ( ! $this->is_addon() ) {
1448 add_action( 'init', array( &$this, '_add_default_submenu_items' ), WP_FS__LOWEST_PRIORITY );
1449 }
1450 }
1451
1452 if ( $this->_storage->handle_gdpr_admin_notice ) {
1453 add_action( 'init', array( &$this, '_maybe_show_gdpr_admin_notice' ) );
1454 }
1455
1456 add_action( 'init', array( &$this, '_maybe_add_gdpr_optin_ajax_handler') );
1457 add_action( 'init', array( &$this, '_maybe_add_pricing_ajax_handler' ) );
1458 }
1459
1460 if ( $this->is_plugin() ) {
1461 if ( version_compare( $GLOBALS['wp_version'], '5.1', '<' ) ) {
1462 add_action( 'wpmu_new_blog', array( $this, '_after_new_blog_callback' ), 10, 6 );
1463 } else {
1464 add_action( 'wp_initialize_site', array( $this, '_after_wp_initialize_site_callback' ), 11, 2 );
1465 }
1466
1467 register_deactivation_hook( $this->_plugin_main_file_path, array( &$this, '_deactivate_plugin_hook' ) );
1468 }
1469
1470 if ( is_multisite() ) {
1471 add_action( 'deactivate_blog', array( &$this, '_after_site_deactivated_callback' ) );
1472 add_action( 'archive_blog', array( &$this, '_after_site_deactivated_callback' ) );
1473 add_action( 'make_spam_blog', array( &$this, '_after_site_deactivated_callback' ) );
1474
1475 if ( version_compare( $GLOBALS['wp_version'], '5.1', '<' ) ) {
1476 add_action( 'deleted_blog', array( $this, '_after_site_deleted_callback' ), 10, 2 );
1477 } else {
1478 add_action( 'wp_delete_site', array( $this, '_after_wpsite_deleted_callback' ) );
1479 }
1480
1481 add_action( 'activate_blog', array( &$this, '_after_site_reactivated_callback' ) );
1482 add_action( 'unarchive_blog', array( &$this, '_after_site_reactivated_callback' ) );
1483 add_action( 'make_ham_blog', array( &$this, '_after_site_reactivated_callback' ) );
1484 }
1485
1486 if ( $this->is_theme() &&
1487 self::is_customizer() &&
1488 $this->apply_filters( 'show_customizer_upsell', true )
1489 ) {
1490 // Register customizer upsell.
1491 add_action( 'customize_register', array( &$this, '_customizer_register' ) );
1492 }
1493
1494 add_action( 'admin_init', array( &$this, '_redirect_on_clicked_menu_link' ), WP_FS__LOWEST_PRIORITY );
1495
1496 if ( $this->is_theme() && ! $this->is_migration() ) {
1497 add_action( 'admin_init', array( &$this, '_add_tracking_links' ) );
1498 }
1499
1500 add_action( 'admin_init', array( &$this, '_add_license_activation' ) );
1501 add_action( 'admin_init', array( &$this, '_add_premium_version_upgrade_selection' ) );
1502 add_action( 'admin_init', array( &$this, '_add_beta_mode_update_handler' ) );
1503 add_action( 'admin_init', array( &$this, '_add_user_change_option' ) );
1504 add_action( 'admin_init', array( &$this, '_add_email_address_update_option' ) );
1505
1506 $this->add_ajax_action( 'update_billing', array( &$this, '_update_billing_ajax_action' ) );
1507 $this->add_ajax_action( 'start_trial', array( &$this, '_start_trial_ajax_action' ) );
1508 $this->add_ajax_action( 'set_data_debug_mode', array( &$this, '_set_data_debug_mode' ) );
1509 $this->add_ajax_action( 'toggle_whitelabel_mode', array( &$this, '_toggle_whitelabel_mode_ajax_handler' ) );
1510
1511 if ( $this->_is_network_active && fs_is_network_admin() ) {
1512 $this->add_ajax_action( 'network_activate', array( &$this, '_network_activate_ajax_action' ) );
1513 }
1514
1515 $this->add_ajax_action( 'install_premium_version', array(
1516 &$this,
1517 '_install_premium_version_ajax_action'
1518 ) );
1519
1520 $this->add_ajax_action( 'submit_affiliate_application', array( &$this, '_submit_affiliate_application' ) );
1521
1522 $this->add_action( 'after_plans_sync', array( &$this, '_check_for_trial_plans' ) );
1523
1524 $this->add_action( 'sdk_version_update', array( &$this, '_sdk_version_update' ), WP_FS__DEFAULT_PRIORITY, 2 );
1525
1526 $this->add_action(
1527 'plugin_version_update',
1528 array( &$this, '_after_version_update' ),
1529 WP_FS__DEFAULT_PRIORITY,
1530 2
1531 );
1532 $this->add_filter( 'after_code_type_change', array( &$this, '_after_code_type_change' ) );
1533
1534 add_action( 'admin_init', array( &$this, '_add_trial_notice' ) );
1535 add_action( 'admin_init', array( &$this, '_add_affiliate_program_notice' ) );
1536 add_action( 'admin_enqueue_scripts', array( &$this, '_enqueue_common_css' ) );
1537
1538 /**
1539 * Handle request to reset anonymous mode for `get_reconnect_url()`.
1540 *
1541 * @author Vova Feldman (@svovaf)
1542 * @since 1.2.1.5
1543 */
1544 if ( fs_request_is_action( 'reset_anonymous_mode' ) &&
1545 $this->get_unique_affix() === fs_request_get( 'fs_unique_affix' )
1546 ) {
1547 add_action( 'admin_init', array( &$this, 'connect_again' ) );
1548 }
1549 }
1550
1551 /**
1552 * Register the required hooks right after the settings parse is completed.
1553 *
1554 * @author Vova Feldman (@svovaf)
1555 * @since 2.3.1
1556 */
1557 private function register_after_settings_parse_hooks() {
1558 if ( is_admin() &&
1559 $this->is_theme() &&
1560 $this->is_premium() &&
1561 ! $this->has_active_valid_license()
1562 ) {
1563 $this->add_ajax_action(
1564 'delete_theme_update_data',
1565 array( &$this, '_delete_theme_update_data_action' )
1566 );
1567 }
1568
1569 if ( $this->show_settings_with_tabs() ) {
1570 /**
1571 * Include the required hooks to capture the theme settings' page tabs
1572 * and cache them.
1573 *
1574 * @author Vova Feldman (@svovaf)
1575 * @since 1.2.2.7
1576 */
1577 if ( ! $this->_cache->has_valid( 'tabs' ) ) {
1578 add_action( 'admin_footer', array( &$this, '_tabs_capture' ) );
1579 // Add license activation AJAX callback.
1580 $this->add_ajax_action( 'store_tabs', array( &$this, '_store_tabs_ajax_action' ) );
1581
1582 add_action( 'admin_enqueue_scripts', array( &$this, '_store_tabs_styles' ), 9999999 );
1583 }
1584
1585 add_action(
1586 'admin_footer',
1587 array( &$this, '_add_freemius_tabs' ),
1588 /**
1589 * The tabs JS code must be executed after the tabs capture logic (_tabs_capture()).
1590 * That's why the priority is 11 while the tabs capture logic is added
1591 * with priority 10.
1592 *
1593 * @author Vova Feldman (@svovaf)
1594 */
1595 11
1596 );
1597 }
1598
1599 if ( ! self::is_ajax() ) {
1600 if ( ! $this->is_addon() || $this->is_only_premium() ) {
1601 add_action(
1602 ( $this->_is_network_active && fs_is_network_admin() ? 'network_' : '' ) . 'admin_menu',
1603 array( &$this, '_prepare_admin_menu' ),
1604 WP_FS__LOWEST_PRIORITY
1605 );
1606 }
1607 }
1608 }
1609
1610 /**
1611 * Makes Freemius-related updates unavailable on the "Add Plugins" admin page (/plugin-install.php) so that
1612 * they won't interfere with the .org plugins' functionalities on that page (e.g. updating of a .org plugin).
1613 *
1614 * @author Leo Fajardo (@leorw)
1615 * @since 2.2.3
1616 *
1617 * @param object $updates
1618 * @param string|null $transient
1619 *
1620 * @return object
1621 */
1622 static function _remove_fs_updates_from_plugin_install_page( $updates, $transient = null ) {
1623 if ( is_object( $updates ) && isset( $updates->response ) ) {
1624 foreach ( $updates->response as $file => $plugin ) {
1625 if ( isset( $plugin->package ) && false !== strpos( $plugin->package, 'api.freemius' ) ) {
1626 unset( $updates->response[ $file ] );
1627 }
1628 }
1629 }
1630
1631 return $updates;
1632 }
1633
1634 /**
1635 * Prepends the `fs_allow_updater_and_dialog` param to the plugin information URLs to tell the SDK to handle
1636 * the information that is shown on the plugin details dialog that is shown when the relevant link is clicked.
1637 *
1638 * @author Leo Fajardo (@leorw)
1639 * @since 2.2.3
1640 *
1641 * @return string
1642 */
1643 static function _prepend_fs_allow_updater_and_dialog_flag_url_param() {
1644 $slug_basename_map = array();
1645 foreach ( self::$_instances as $instance ) {
1646 if ( ! $instance->is_plugin() ) {
1647 continue;
1648 }
1649
1650 $slug_basename_map[ $instance->get_slug() ] = $instance->premium_plugin_basename();
1651 }
1652 ?>
1653 <script type="text/javascript">
1654 (function( $ ) {
1655 var slugBasenameMap = <?php echo json_encode( $slug_basename_map ) ?>;
1656 for ( var slug in slugBasenameMap ) {
1657 var basename = slugBasenameMap[ slug ];
1658
1659 // Try to get the plugin rows if on the "Plugins" page.
1660 var $pluginRows = $( '.wp-list-table.plugins tr[data-plugin="' + basename + '"]');
1661
1662 if ( 0 === $pluginRows.length ) {
1663 // Try to get the plugin rows if on the "Updates" page.
1664 var $pluginCheckbox = $( '#update-plugins-table input[type="checkbox"][value="' + basename + '"]' );
1665 if ( 0 !== $pluginCheckbox.length ) {
1666 $pluginRows = $pluginCheckbox.parents( 'tr:first' );
1667 }
1668 }
1669
1670 if ( 0 === $pluginRows.length ) {
1671 // No plugin rows found.
1672 continue;
1673 }
1674
1675 // Find the "View details" links and add the `fs_allow_updater_and_dialog` param to the URL.
1676 $pluginRows.find( 'a[href*="plugin-install.php?tab=plugin-information"]' ).each(function() {
1677 var $this = $( this ),
1678 href = $this.attr( 'href' ).replace( '?tab=', '?fs_allow_updater_and_dialog=true&tab=');
1679
1680 $this.attr( 'href', href );
1681 });
1682 }
1683 })( jQuery );
1684 </script>
1685 <?php
1686 }
1687
1688 /**
1689 * @author Leo Fajardo (@leorw)
1690 * @since 2.3.0
1691 */
1692 static function _maybe_add_beta_label_styles() {
1693 $has_any_beta_version = false;
1694
1695 foreach ( self::$_instances as $instance ) {
1696 if ( $instance->is_beta() ) {
1697 $has_any_beta_version = true;
1698 break;
1699 }
1700 }
1701
1702 if ( $has_any_beta_version ) {
1703 fs_enqueue_local_style( 'fs_plugins', '/admin/plugins.css' );
1704 }
1705 }
1706
1707 /**
1708 * @author Leo Fajardo (@leorw)
1709 * @since 2.3.0
1710 */
1711 static function _maybe_add_beta_label_to_plugins_and_handle_confirmation() {
1712 $beta_data = array();
1713
1714 foreach ( self::$_instances as $instance ) {
1715 if ( ! $instance->is_premium() ) {
1716 continue;
1717 }
1718
1719 /**
1720 * If there's an available beta version update, a confirmation message will be shown when the
1721 * "Update now" link on the "Plugins" or "Themes" page is clicked.
1722 */
1723 $has_beta_update = $instance->has_beta_update();
1724
1725 $is_beta = (
1726 // The "Beta" label is added separately for themes.
1727 $instance->is_plugin() &&
1728 $instance->is_beta()
1729 );
1730
1731 if ( ! $is_beta && ! $has_beta_update ) {
1732 continue;
1733 }
1734
1735 $beta_data[ $instance->get_plugin_basename() ] = array( 'is_installed_version_beta' => $is_beta );
1736
1737 if ( ! $has_beta_update ) {
1738 continue;
1739 }
1740
1741 $beta_data[ $instance->get_plugin_basename() ]['beta_version_update_confirmation_message'] = sprintf(
1742 '%s %s',
1743 sprintf(
1744 fs_esc_attr_inline(
1745 '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.',
1746 'beta-version-update-caution',
1747 $instance->get_slug()
1748 ),
1749 $instance->get_plugin_title()
1750 ),
1751 fs_esc_attr_inline( 'Would you like to proceed with the update?', 'update-confirmation', $instance->get_slug() )
1752 );
1753 }
1754
1755 if ( empty( $beta_data ) ) {
1756 return;
1757 }
1758 ?>
1759 <script type="text/javascript">
1760 ( function( $ ) {
1761 var betaData = <?php echo json_encode( $beta_data ) ?>;
1762
1763 for ( var pluginBasename in betaData ) {
1764 if ( ! betaData.hasOwnProperty( pluginBasename ) ) {
1765 continue;
1766 }
1767
1768 if ( ! betaData[ pluginBasename ].is_installed_version_beta ) {
1769 continue;
1770 }
1771
1772 var $parentContainer = $( '.wp-list-table.plugins tr[data-plugin="' + pluginBasename + '"]' );
1773 if ( 0 === $parentContainer.length ) {
1774 continue;
1775 }
1776
1777 $parentContainer.find( '.plugin-title > strong:first-child').append(
1778 '<span class="fs-tag fs-info"><?php fs_esc_js_echo_inline( 'Beta', 'beta' ) ?></span>'
1779 );
1780 }
1781
1782 setTimeout( function() {
1783 // Wait a little bit before adding the event handler, otherwise, it will be overridden by the core WP logic.
1784 $( '.plugins .update-message .update-link, .themes .theme .update-message' ).on( 'click', function() {
1785 var $parentContainer = $( this ).parents( 'tr:first' );
1786 pluginBasename = ( 0 !== $parentContainer.length ) ?
1787 $parentContainer.data( 'plugin' ) :
1788 $( this ).parents( '.theme:first' ).data( 'slug' );
1789
1790 if (
1791 betaData[ pluginBasename ] &&
1792 betaData[ pluginBasename ].beta_version_update_confirmation_message &&
1793 ! confirm( betaData[ pluginBasename ].beta_version_update_confirmation_message )
1794 ) {
1795 return false;
1796 }
1797 } );
1798 }, 20 );
1799 } )( jQuery );
1800 </script>
1801 <?php
1802 }
1803
1804 /**
1805 * Keeping the uninstall hook registered for free or premium plugin version may result to a fatal error that
1806 * could happen when a user tries to uninstall either version while one of them is still active. Uninstalling a
1807 * plugin will trigger inclusion of the free or premium version and if one of them is active during the
1808 * uninstallation, a fatal error may occur in case the plugin's class or functions are already defined.
1809 *
1810 * @author Leo Fajardo (@leorw)
1811 *
1812 * @since 1.2.0
1813 */
1814 private function unregister_uninstall_hook() {
1815 $uninstallable_plugins = (array) get_option( 'uninstall_plugins' );
1816 unset( $uninstallable_plugins[ $this->_free_plugin_basename ] );
1817 unset( $uninstallable_plugins[ $this->premium_plugin_basename() ] );
1818
1819 update_option( 'uninstall_plugins', $uninstallable_plugins );
1820 }
1821
1822 /**
1823 * @since 1.2.0 Invalidate module's main file cache, otherwise, FS_Plugin_Updater will not fetch updates.
1824 *
1825 * @param bool $store_prev_path
1826 */
1827 private function clear_module_main_file_cache( $store_prev_path = true ) {
1828 if ( ! isset( $this->_storage->plugin_main_file ) ||
1829 empty( $this->_storage->plugin_main_file->path )
1830 ) {
1831 return;
1832 }
1833
1834 if ( ! $store_prev_path ) {
1835 /**
1836 * Storing the previous path is not needed when clearing the cache after an SDK version update since
1837 * the main purpose of the cache clearing in that event is to correct a wrong plugin main file path
1838 * which causes data mix-up between plugins (e.g. titles and versions of an add-on and its parent plugin).
1839 *
1840 * @author Leo Fajardo (@leorw)
1841 * @since 2.2.1
1842 */
1843 unset( $this->_storage->plugin_main_file->path );
1844 } else {
1845 $plugin_main_file = clone $this->_storage->plugin_main_file;
1846
1847 // Store cached path (2nd layer cache).
1848 $plugin_main_file->prev_path = $plugin_main_file->path;
1849
1850 // Clear cached path.
1851 unset( $plugin_main_file->path );
1852
1853 $this->_storage->plugin_main_file = $plugin_main_file;
1854 }
1855
1856 /**
1857 * Clear global cached path.
1858 *
1859 * @author Leo Fajardo (@leorw)
1860 * @since 1.2.2
1861 */
1862 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map' );
1863 unset( $id_slug_type_path_map[ $this->_module_id ]['path'] );
1864 self::$_accounts->set_option( 'id_slug_type_path_map', $id_slug_type_path_map, true );
1865 }
1866
1867 /**
1868 * @author Leo Fajardo (@leorw)
1869 * @since 2.0.0
1870 */
1871 function _hook_action_links_and_register_account_hooks() {
1872 if ( $this->is_migration() ) {
1873 return;
1874 }
1875
1876 if (
1877 ( self::is_plugins_page() && $this->is_plugin() ) ||
1878 ( self::is_themes_page() && $this->is_theme() ) ||
1879 fs_request_is_action_secure( $this->get_unique_affix() . '_reconnect' )
1880 ) {
1881 $this->_add_tracking_links();
1882 }
1883
1884 if ( self::is_plugins_page() && $this->is_plugin() ) {
1885 $this->hook_plugin_action_links();
1886 }
1887
1888 $this->_register_account_hooks();
1889 }
1890
1891 /**
1892 * @author Vova Feldman (@svovaf)
1893 * @since 1.0.9
1894 */
1895 private function _register_account_hooks() {
1896 if ( ! is_admin() ) {
1897 return;
1898 }
1899
1900 /**
1901 * Always show the deactivation feedback form since we added
1902 * automatic free version deactivation upon premium code activation.
1903 *
1904 * @since 1.2.1.6
1905 */
1906 $this->add_ajax_action(
1907 'submit_uninstall_reason',
1908 array( &$this, '_submit_uninstall_reason_action' )
1909 );
1910
1911 $this->add_ajax_action(
1912 'cancel_subscription_or_trial',
1913 array( &$this, 'cancel_subscription_or_trial_ajax_action' )
1914 );
1915
1916 if ( ! $this->is_addon() || $this->is_parent_plugin_installed() ) {
1917 if ( ( $this->is_plugin() && self::is_plugins_page() ) ||
1918 ( $this->is_theme() && self::is_themes_page() )
1919 ) {
1920 add_action( 'admin_footer', array( &$this, '_add_deactivation_feedback_dialog_box' ) );
1921 }
1922 }
1923 }
1924
1925 /**
1926 * Leverage backtrace to find caller plugin file path.
1927 *
1928 * @param bool $is_init Is initiation sequence.
1929 * @param string $main_file Since 2.5.0 expects the module's main file path to potentially purge the cached path.
1930 *
1931 * @return string
1932 * @since 1.0.6
1933 *
1934 * @author Vova Feldman (@svovaf)
1935 */
1936 private function _find_caller_plugin_file( $is_init = false, $main_file = '' ) {
1937 // Try to load the cached value of the file path.
1938 if ( isset( $this->_storage->plugin_main_file ) ) {
1939 $plugin_main_file = $this->_storage->plugin_main_file;
1940 if ( ! empty( $plugin_main_file->path ) ) {
1941 $absolute_path = $this->get_absolute_path( $plugin_main_file->path );
1942 if ( file_exists( $absolute_path ) ) {
1943 if ( $is_init && $absolute_path !== $this->get_absolute_path( $main_file ) ) {
1944 // Update cached path if not matching the actual path.
1945 $plugin_main_file->path = $main_file;
1946 $this->_storage->plugin_main_file = $plugin_main_file;
1947 }
1948
1949 return $absolute_path;
1950 }
1951 }
1952 }
1953
1954 /**
1955 * @since 1.2.1
1956 *
1957 * `clear_module_main_file_cache()` is clearing the plugin's cached path on
1958 * deactivation. Therefore, if any plugin/theme was initiating `Freemius`
1959 * with that plugin's slug, it was overriding the empty plugin path with a wrong path.
1960 *
1961 * So, we've added a special mechanism with a 2nd layer of cache that uses `prev_path`
1962 * when the class instantiator isn't the module.
1963 */
1964 if ( ! $is_init ) {
1965 // Fetch prev path cache.
1966 if ( isset( $this->_storage->plugin_main_file ) &&
1967 ! empty( $this->_storage->plugin_main_file->prev_path )
1968 ) {
1969 $absolute_path = $this->get_absolute_path( $this->_storage->plugin_main_file->prev_path );
1970 if ( file_exists( $absolute_path ) ) {
1971 return $absolute_path;
1972 }
1973 }
1974
1975 wp_die(
1976 $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' ) .
1977 " Module: {$this->_slug}; SDK: " . WP_FS__SDK_VERSION . ";",
1978 $this->get_text_inline( 'Error', 'error' ),
1979 array( 'back_link' => true )
1980 );
1981 }
1982
1983 /**
1984 * @since 1.2.1
1985 *
1986 * Only the original instantiator that calls dynamic_init can modify the module's path.
1987 */
1988 // Find caller module.
1989 $this->_storage->plugin_main_file = (object) array(
1990 'path' => $main_file,
1991 );
1992
1993 return $this->get_absolute_path( $main_file );
1994 }
1995
1996 /**
1997 * @author Leo Fajardo (@leorw)
1998 * @since 1.2.3
1999 *
2000 * @param string $path
2001 *
2002 * @return string
2003 */
2004 private function get_relative_path( $path ) {
2005 $module_root_dir = $this->get_module_root_dir_path();
2006 if ( 0 === strpos( $path, $module_root_dir ) ) {
2007 $path = substr( $path, strlen( $module_root_dir ) );
2008 }
2009
2010 return $path;
2011 }
2012
2013 /**
2014 * @author Leo Fajardo (@leorw)
2015 * @since 1.2.3
2016 *
2017 * @param string $path
2018 * @param string|bool $module_type
2019 *
2020 * @return string
2021 */
2022 private function get_absolute_path( $path, $module_type = false ) {
2023 $module_root_dir = $this->get_module_root_dir_path( $module_type );
2024 if ( 0 !== strpos( $path, $module_root_dir ) ) {
2025 $path = fs_normalize_path( $module_root_dir . $path );
2026 }
2027
2028 return $path;
2029 }
2030
2031 /**
2032 * @author Leo Fajardo (@leorw)
2033 * @since 1.2.3
2034 *
2035 * @param string|bool $module_type
2036 *
2037 * @return string
2038 */
2039 private function get_module_root_dir_path( $module_type = false ) {
2040 $is_plugin = empty( $module_type ) ?
2041 $this->is_plugin() :
2042 ( WP_FS__MODULE_TYPE_PLUGIN === $module_type );
2043
2044 return fs_normalize_path( trailingslashit( $is_plugin ?
2045 WP_PLUGIN_DIR :
2046 get_theme_root( get_stylesheet() ) ) );
2047 }
2048
2049 /**
2050 * @author Leo Fajardo (@leorw)
2051 *
2052 * @param number $module_id
2053 * @param string $slug
2054 *
2055 * @return string Since 2.5.0 return the module's main file path.
2056 *
2057 * @since 1.2.2
2058 */
2059 private function store_id_slug_type_path_map( $module_id, $slug ) {
2060 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
2061
2062 $store_option = false;
2063
2064 if ( ! isset( $id_slug_type_path_map[ $module_id ] ) ) {
2065 $id_slug_type_path_map[ $module_id ] = array(
2066 'slug' => $slug
2067 );
2068
2069 $store_option = true;
2070 } else if (
2071 isset( $id_slug_type_path_map[ $module_id ]['slug'] ) &&
2072 $slug !== $id_slug_type_path_map[ $module_id ]['slug']
2073 ) {
2074 $id_slug_type_path_map[ $module_id ]['slug'] = $slug;
2075 $store_option = true;
2076 }
2077
2078 $find_caller = empty( $id_slug_type_path_map[ $module_id ]['path'] );
2079
2080 if ( ! $find_caller ) {
2081 /**
2082 * This verification is for cases when suddenly the same module
2083 * is installed but with a different folder name.
2084 *
2085 * @author Vova Feldman (@svovaf)
2086 * @since 1.2.3
2087 */
2088 $find_caller = ! file_exists( $this->get_absolute_path(
2089 $id_slug_type_path_map[ $module_id ]['path'],
2090 $id_slug_type_path_map[ $module_id ]['type']
2091 ) );
2092 }
2093
2094 foreach ( $id_slug_type_path_map as $id => $data ) {
2095 if ( empty( $id ) ) {
2096 // Remove maps with empty module ID.
2097 unset( $id_slug_type_path_map[ $id ] );
2098 $store_option = true;
2099 continue;
2100 }
2101
2102 /**
2103 * 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.
2104 *
2105 * @author Vova Feldman (@svovaf)
2106 * @since 2.5.0
2107 */
2108 if ( ! $find_caller ) {
2109 if ( $id == $module_id ) {
2110 continue;
2111 }
2112
2113 if (
2114 isset( $data['path'] ) &&
2115 $data['path'] === $id_slug_type_path_map[ $module_id ]['path']
2116 ) {
2117 $find_caller = true;
2118 }
2119 }
2120 }
2121
2122 if ( $find_caller ) {
2123 $caller_main_file_and_type = $this->get_caller_main_file_and_type( $module_id );
2124
2125 $id_slug_type_path_map[ $module_id ]['type'] = $caller_main_file_and_type->module_type;
2126 $id_slug_type_path_map[ $module_id ]['path'] = $caller_main_file_and_type->path;
2127
2128 $store_option = true;
2129 }
2130
2131 if ( $store_option ) {
2132 self::$_accounts->set_option( 'id_slug_type_path_map', $id_slug_type_path_map, true );
2133 }
2134
2135 return $id_slug_type_path_map[ $module_id ]['path'];
2136 }
2137
2138 /**
2139 * Identifies the caller type: plugin or theme.
2140 *
2141 * @author Leo Fajardo (@leorw)
2142 * @since 1.2.2
2143 *
2144 * @author Vova Feldman (@svovaf)
2145 * @since 1.2.2.3 Find the earliest module in the call stack that calls to the SDK. This fix is for cases when
2146 * add-ons are relying on loading the SDK from the parent module, and also allows themes including the
2147 * SDK an internal file instead of directly from functions.php.
2148 * @since 1.2.1.7 Knows how to handle cases when an add-on includes the parent module logic.
2149 *
2150 * @param number $module_id @since 2.5.0
2151 */
2152 private function get_caller_main_file_and_type( $module_id ) {
2153 self::require_plugin_essentials();
2154
2155 $all_plugins = fs_get_plugins( true );
2156 $all_plugins_paths = array();
2157
2158 // Get active plugin's main files real full names (might be symlinks).
2159 foreach ( $all_plugins as $relative_path => $data ) {
2160 if ( false === strpos( fs_normalize_path( $relative_path ), '/' ) ) {
2161 /**
2162 * Ignore plugins that don't have a folder (e.g. Hello Dolly) since they
2163 * can't really include the SDK.
2164 *
2165 * @author Vova Feldman
2166 * @since 1.2.1.7
2167 */
2168 continue;
2169 }
2170
2171 $all_plugins_paths[] = fs_normalize_path( realpath( WP_PLUGIN_DIR . '/' . $relative_path ) );
2172 }
2173
2174 $caller_file_candidate = false;
2175 $caller_map = array();
2176 $module_type = WP_FS__MODULE_TYPE_PLUGIN;
2177 $themes_dir = fs_normalize_path( get_theme_root( get_stylesheet() ) );
2178 $plugin_dir_to_skip = false;
2179
2180 for ( $i = 1, $bt = debug_backtrace(), $len = count( $bt ); $i < $len; $i ++ ) {
2181 if ( empty( $bt[ $i ]['file'] ) ) {
2182 continue;
2183 }
2184
2185 if ( $i > 1 && ! empty( $bt[ $i - 1 ]['file'] ) && $bt[ $i ]['file'] === $bt[ $i - 1 ]['file'] ) {
2186 // If file same as the prev file in the stack, skip it.
2187 continue;
2188 }
2189
2190 if ( ! empty( $bt[ $i ]['function'] ) && in_array( $bt[ $i ]['function'], array(
2191 'do_action',
2192 'apply_filter',
2193 // The string split is stupid, but otherwise, theme check
2194 // throws info notices.
2195 'requir' . 'e_once',
2196 'requir' . 'e',
2197 'includ' . 'e_once',
2198 'includ' . 'e',
2199 'install_and_activate_plugin',
2200 'try_activate_plugin',
2201 'activate_plugin'
2202 ) )
2203 ) {
2204 if ( 'activate_plugin' === $bt[ $i ]['function'] ) {
2205 /**
2206 * Store the directory of the activator plugin so that any other file that starts with it
2207 * cannot be mistakenly chosen as a candidate caller file.
2208 *
2209 * @author Leo Fajardo
2210 *
2211 * @since 2.3.0
2212 */
2213 $caller_file_path = fs_normalize_path( $bt[ $i ]['file'] );
2214
2215 foreach ( $all_plugins_paths as $plugin_path ) {
2216 $plugin_dir = fs_normalize_path( dirname( $plugin_path ) . '/' );
2217 if ( false !== strpos( $caller_file_path, $plugin_dir ) ) {
2218 $plugin_dir_to_skip = $plugin_dir;
2219
2220 break;
2221 }
2222 }
2223 }
2224
2225 // Ignore call stack hooks and files inclusion.
2226 continue;
2227 }
2228
2229 $caller_file_path = fs_normalize_path( $bt[ $i ]['file'] );
2230
2231 if ( ! empty( $plugin_dir_to_skip ) ) {
2232 /**
2233 * Skip if it's an activator plugin file to avoid mistakenly choosing it as a candidate caller file.
2234 *
2235 * @author Leo Fajardo
2236 *
2237 * @since 2.3.0
2238 */
2239 if ( 0 === strpos( $caller_file_path, $plugin_dir_to_skip ) ) {
2240 continue;
2241 }
2242 }
2243
2244 if ( 'functions.php' === basename( $caller_file_path ) ) {
2245 /**
2246 * 1. Assumes that theme's starting execution file is functions.php.
2247 * 2. This complex logic fixes symlink issues (e.g. with Vargant).
2248 *
2249 * @author Vova Feldman (@svovaf)
2250 * @since 1.2.2.5
2251 */
2252
2253 if ( $caller_file_path == fs_normalize_path( realpath( trailingslashit( $themes_dir ) . basename( dirname( $caller_file_path ) ) . '/' . basename( $caller_file_path ) ) ) ) {
2254 $module_type = WP_FS__MODULE_TYPE_THEME;
2255
2256 /**
2257 * Relative path of the theme, e.g.:
2258 * `my-theme/functions.php`
2259 *
2260 * @author Leo Fajardo (@leorw)
2261 */
2262 $caller_file_candidate = basename( dirname( $caller_file_path ) ) .
2263 '/' .
2264 basename( $caller_file_path );
2265
2266 continue;
2267 }
2268 }
2269
2270 $caller_file_hash = md5( $caller_file_path );
2271
2272 if ( ! isset( $caller_map[ $caller_file_hash ] ) ) {
2273 foreach ( $all_plugins_paths as $plugin_path ) {
2274 if ( empty( $plugin_path ) ) {
2275 continue;
2276 }
2277
2278 if ( false !== strpos( $caller_file_path, fs_normalize_path( dirname( $plugin_path ) . '/' ) ) ) {
2279 $caller_map[ $caller_file_hash ] = fs_normalize_path( $plugin_path );
2280 break;
2281 }
2282 }
2283 }
2284
2285 if ( isset( $caller_map[ $caller_file_hash ] ) ) {
2286 $module_type = WP_FS__MODULE_TYPE_PLUGIN;
2287 $caller_file_candidate = plugin_basename( $caller_map[ $caller_file_hash ] );
2288 }
2289 }
2290
2291 $caller_main_file_and_type = (object) array(
2292 'module_type' => $module_type,
2293 'path' => $caller_file_candidate
2294 );
2295
2296 return apply_filters( "fs_{$module_id}_caller_main_file_and_type", $caller_main_file_and_type );
2297 }
2298
2299 #----------------------------------------------------------------------------------
2300 #region Deactivation Feedback Form
2301 #----------------------------------------------------------------------------------
2302
2303 /**
2304 * Displays a confirmation and feedback dialog box when the user clicks on the "Deactivate" link on the plugins
2305 * page.
2306 *
2307 * @author Vova Feldman (@svovaf)
2308 * @author Leo Fajardo (@leorw)
2309 *
2310 * @since 1.1.2
2311 */
2312 function _add_deactivation_feedback_dialog_box() {
2313 if (
2314 $this->is_clone() ||
2315 ( is_object( $this->_site ) && ! $this->is_registered() )
2316 ) {
2317 return;
2318 }
2319
2320 $subscription_cancellation_dialog_box_template_params = $this->apply_filters( 'show_deactivation_subscription_cancellation', true ) ?
2321 $this->_get_subscription_cancellation_dialog_box_template_params() :
2322 array();
2323
2324 /**
2325 * @since 2.3.0 Developers can optionally hide the deactivation feedback form using the 'show_deactivation_feedback_form' filter.
2326 */
2327 $show_deactivation_feedback_form = ! self::is_deactivation_snoozed();
2328 if ( $this->has_filter( 'show_deactivation_feedback_form' ) ) {
2329 $show_deactivation_feedback_form = $this->apply_filters( 'show_deactivation_feedback_form', true );
2330 } else if ( $this->is_addon() ) {
2331 /**
2332 * If the add-on's 'show_deactivation_feedback_form' is not set, try to inherit the value from the parent.
2333 */
2334 $show_deactivation_feedback_form = $this->get_parent_instance()->apply_filters( 'show_deactivation_feedback_form', true );
2335 }
2336
2337 $uninstall_confirmation_message = $this->apply_filters( 'uninstall_confirmation_message', '' );
2338
2339 if (
2340 empty( $subscription_cancellation_dialog_box_template_params ) &&
2341 ! $show_deactivation_feedback_form &&
2342 empty( $uninstall_confirmation_message )
2343 ) {
2344 return;
2345 }
2346
2347 $vars = array( 'id' => $this->_module_id );
2348
2349 if ( $show_deactivation_feedback_form ) {
2350 /* Check the type of user:
2351 * 1. Long-term (long-term)
2352 * 2. Non-registered and non-anonymous short-term (non-registered-and-non-anonymous-short-term).
2353 * 3. Short-term (short-term)
2354 */
2355 $is_long_term_user = true;
2356
2357 // Check if the site is at least 2 days old.
2358 $time_installed = $this->_storage->install_timestamp;
2359
2360 // Difference in seconds.
2361 $date_diff = time() - $time_installed;
2362
2363 // Convert seconds to days.
2364 $date_diff_days = floor( $date_diff / ( 60 * 60 * 24 ) );
2365
2366 if ( $date_diff_days < 2 ) {
2367 $is_long_term_user = false;
2368 }
2369
2370 $is_long_term_user = $this->apply_filters( 'is_long_term_user', $is_long_term_user );
2371
2372 if ( $is_long_term_user ) {
2373 $user_type = 'long-term';
2374 } else {
2375 if ( ! $this->is_registered() && ! $this->is_anonymous() ) {
2376 $user_type = 'non-registered-and-non-anonymous-short-term';
2377 } else {
2378 $user_type = 'short-term';
2379 }
2380 }
2381
2382 $uninstall_reasons = $this->_get_uninstall_reasons( $user_type );
2383
2384 $vars['reasons'] = $uninstall_reasons;
2385 }
2386
2387 $vars['subscription_cancellation_dialog_box_template_params'] = &$subscription_cancellation_dialog_box_template_params;
2388 $vars['show_deactivation_feedback_form'] = $show_deactivation_feedback_form;
2389 $vars['uninstall_confirmation_message'] = $uninstall_confirmation_message;
2390
2391 /**
2392 * Load the HTML template for the deactivation feedback dialog box.
2393 *
2394 * @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.
2395 */
2396 fs_require_template( 'forms/deactivation/form.php', $vars );
2397 }
2398
2399 /**
2400 * @author Leo Fajardo (@leorw)
2401 * @since 1.1.2
2402 *
2403 * @param string $user_type
2404 *
2405 * @return array The uninstall reasons for the specified user type.
2406 */
2407 function _get_uninstall_reasons( $user_type = 'long-term' ) {
2408 $module_type = $this->_module_type;
2409
2410 $internal_message_template_var = array(
2411 'id' => $this->_module_id
2412 );
2413
2414 $plan = $this->get_plan();
2415
2416 if ( $this->is_registered() && is_object( $plan ) && $plan->has_technical_support() ) {
2417 $contact_support_template = fs_get_template( 'forms/deactivation/contact.php', $internal_message_template_var );
2418 } else {
2419 $contact_support_template = '';
2420 }
2421
2422 $reason_found_better_plugin = array(
2423 'id' => self::REASON_FOUND_A_BETTER_PLUGIN,
2424 'text' => sprintf( $this->get_text_inline( 'I found a better %s', 'reason-found-a-better-plugin' ), $module_type ),
2425 'input_type' => 'textfield',
2426 'input_placeholder' => sprintf( $this->get_text_inline( "What's the %s's name?", 'placeholder-plugin-name' ), $module_type ),
2427 );
2428
2429 $reason_temporary_deactivation = array(
2430 'id' => self::REASON_TEMPORARY_DEACTIVATION,
2431 'text' => sprintf(
2432 $this->get_text_inline( "It's a temporary %s - I'm troubleshooting an issue", 'reason-temporary-x' ),
2433 strtolower( $this->is_plugin() ?
2434 $this->get_text_inline( 'Deactivation', 'deactivation' ) :
2435 $this->get_text_inline( 'Theme Switch', 'theme-switch' )
2436 )
2437 ),
2438 'input_type' => '',
2439 'input_placeholder' => ''
2440 );
2441
2442 $reason_other = array(
2443 'id' => self::REASON_OTHER,
2444 'text' => $this->get_text_inline( 'Other', 'reason-other' ),
2445 'input_type' => 'textfield',
2446 'input_placeholder' => ''
2447 );
2448
2449 $long_term_user_reasons = array(
2450 array(
2451 'id' => self::REASON_NO_LONGER_NEEDED,
2452 'text' => sprintf( $this->get_text_inline( 'I no longer need the %s', 'reason-no-longer-needed' ), $module_type ),
2453 'input_type' => '',
2454 'input_placeholder' => ''
2455 ),
2456 $reason_found_better_plugin,
2457 array(
2458 'id' => self::REASON_NEEDED_FOR_A_SHORT_PERIOD,
2459 'text' => sprintf( $this->get_text_inline( 'I only needed the %s for a short period', 'reason-needed-for-a-short-period' ), $module_type ),
2460 'input_type' => '',
2461 'input_placeholder' => ''
2462 ),
2463 array(
2464 'id' => self::REASON_BROKE_MY_SITE,
2465 'text' => sprintf( $this->get_text_inline( 'The %s broke my site', 'reason-broke-my-site' ), $module_type ),
2466 'input_type' => '',
2467 'input_placeholder' => '',
2468 'internal_message' => $contact_support_template
2469 ),
2470 array(
2471 'id' => self::REASON_SUDDENLY_STOPPED_WORKING,
2472 'text' => sprintf( $this->get_text_inline( 'The %s suddenly stopped working', 'reason-suddenly-stopped-working' ), $module_type ),
2473 'input_type' => '',
2474 'input_placeholder' => '',
2475 'internal_message' => $contact_support_template
2476 )
2477 );
2478
2479 if ( $this->is_paying() ) {
2480 $long_term_user_reasons[] = array(
2481 'id' => self::REASON_CANT_PAY_ANYMORE,
2482 'text' => $this->get_text_inline( "I can't pay for it anymore", 'reason-cant-pay-anymore' ),
2483 'input_type' => 'textfield',
2484 'input_placeholder' => $this->get_text_inline( 'What price would you feel comfortable paying?', 'placeholder-comfortable-price' )
2485 );
2486 }
2487
2488 $reason_dont_share_info = array(
2489 'id' => self::REASON_DONT_LIKE_TO_SHARE_MY_INFORMATION,
2490 'text' => $this->get_text_inline( "I don't like to share my information with you", 'reason-dont-like-to-share-my-information' ),
2491 'input_type' => '',
2492 'input_placeholder' => ''
2493 );
2494
2495 /**
2496 * If the current user has selected the "don't share data" reason in the deactivation feedback modal, inform the
2497 * user by showing additional message that he doesn't have to share data and can just choose to skip the opt-in
2498 * (the Skip button is included in the message to show). This message will only be shown if anonymous mode is
2499 * enabled and the user's account is currently not in pending activation state (similar to the way the Skip
2500 * button in the opt-in form is shown/hidden).
2501 */
2502 if ( $this->is_enable_anonymous() && ! $this->is_pending_activation() ) {
2503 $reason_dont_share_info['internal_message'] = fs_get_template( 'forms/deactivation/retry-skip.php', $internal_message_template_var );
2504 }
2505
2506 $uninstall_reasons = array(
2507 'long-term' => $long_term_user_reasons,
2508 'non-registered-and-non-anonymous-short-term' => array(
2509 array(
2510 'id' => self::REASON_DIDNT_WORK,
2511 'text' => sprintf( $this->get_text_inline( "The %s didn't work", 'reason-didnt-work' ), $module_type ),
2512 'input_type' => '',
2513 'input_placeholder' => ''
2514 ),
2515 $reason_dont_share_info,
2516 $reason_found_better_plugin
2517 ),
2518 'short-term' => array(
2519 array(
2520 'id' => self::REASON_COULDNT_MAKE_IT_WORK,
2521 'text' => $this->get_text_inline( "I couldn't understand how to make it work", 'reason-couldnt-make-it-work' ),
2522 'input_type' => '',
2523 'input_placeholder' => '',
2524 'internal_message' => $contact_support_template
2525 ),
2526 $reason_found_better_plugin,
2527 array(
2528 'id' => self::REASON_GREAT_BUT_NEED_SPECIFIC_FEATURE,
2529 '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 ),
2530 'input_type' => 'textarea',
2531 'input_placeholder' => $this->get_text_inline( 'What feature?', 'placeholder-feature' )
2532 ),
2533 array(
2534 'id' => self::REASON_NOT_WORKING,
2535 'text' => sprintf( $this->get_text_inline( 'The %s is not working', 'reason-not-working' ), $module_type ),
2536 'input_type' => 'textarea',
2537 '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' )
2538 ),
2539 array(
2540 'id' => self::REASON_NOT_WHAT_I_WAS_LOOKING_FOR,
2541 'text' => $this->get_text_inline( "It's not what I was looking for", 'reason-not-what-i-was-looking-for' ),
2542 'input_type' => 'textarea',
2543 'input_placeholder' => $this->get_text_inline( "What you've been looking for?", 'placeholder-what-youve-been-looking-for' )
2544 ),
2545 array(
2546 'id' => self::REASON_DIDNT_WORK_AS_EXPECTED,
2547 'text' => sprintf( $this->get_text_inline( "The %s didn't work as expected", 'reason-didnt-work-as-expected' ), $module_type ),
2548 'input_type' => 'textarea',
2549 'input_placeholder' => $this->get_text_inline( 'What did you expect?', 'placeholder-what-did-you-expect' )
2550 )
2551 )
2552 );
2553
2554 // Randomize the reasons for the current user type.
2555 shuffle( $uninstall_reasons[ $user_type ] );
2556
2557 // Keep the following reasons as the last items in the list.
2558 $uninstall_reasons[ $user_type ][] = $reason_temporary_deactivation;
2559 $uninstall_reasons[ $user_type ][] = $reason_other;
2560
2561 $uninstall_reasons = $this->apply_filters( 'uninstall_reasons', $uninstall_reasons );
2562
2563 return $uninstall_reasons[ $user_type ];
2564 }
2565
2566 /**
2567 * Called after the user has submitted his reason for deactivating the plugin.
2568 *
2569 * @author Leo Fajardo (@leorw)
2570 * @since 1.1.2
2571 */
2572 function _submit_uninstall_reason_action() {
2573 $this->_logger->entrance();
2574
2575 $this->check_ajax_referer( 'submit_uninstall_reason' );
2576
2577 $reason_id = fs_request_get( 'reason_id' );
2578
2579 // Check if the given reason ID is an unsigned integer.
2580 if ( ! ctype_digit( $reason_id ) ) {
2581 exit;
2582 }
2583
2584 $reason_info = trim( fs_request_get( 'reason_info', '' ) );
2585 if ( ! empty( $reason_info ) ) {
2586 $reason_info = substr( $reason_info, 0, 128 );
2587 }
2588
2589 $reason = (object) array(
2590 'id' => $reason_id,
2591 'info' => $reason_info,
2592 'is_anonymous' => fs_request_get_bool( 'is_anonymous' )
2593 );
2594
2595 $this->_storage->store( 'uninstall_reason', $reason );
2596
2597 if ( self::REASON_TEMPORARY_DEACTIVATION == $reason->id ) {
2598 $snooze_period = fs_request_get( 'snooze_period' );
2599
2600 if ( is_numeric( $snooze_period ) && 0 < $snooze_period ) {
2601 self::snooze_deactivation_form( (int) $snooze_period );
2602 }
2603 }
2604
2605 /**
2606 * If the module type is "theme", trigger the uninstall event here (on theme deactivation) since themes do
2607 * not support uninstall hook.
2608 *
2609 * @author Leo Fajardo (@leorw)
2610 * @since 1.2.2
2611 */
2612 if ( $this->is_theme() ) {
2613 if ( $this->is_premium() && ! $this->has_active_valid_license() ) {
2614 FS_Plugin_Updater::instance( $this )->delete_update_data();
2615 }
2616
2617 $this->_uninstall_plugin_event( false );
2618 $this->remove_sdk_reference();
2619 }
2620
2621 // Print '1' for successful operation.
2622 echo 1;
2623 exit;
2624 }
2625
2626 #--------------------------------------------------------------------------------
2627 #region Deactivation Feedback Snoozing
2628 #--------------------------------------------------------------------------------
2629
2630 /**
2631 * @author Vova Feldman (@svovaf)
2632 * @since 2.4.3
2633 *
2634 * @param int $period
2635 *
2636 * @return bool True if the value was set, false otherwise.
2637 */
2638 private static function snooze_deactivation_form( $period ) {
2639 return ( 0 < $period && self::reset_deactivation_snoozing( $period ) );
2640 }
2641
2642 /**
2643 * Check if deactivation feedback form is snoozed.
2644 *
2645 * @author Vova Feldman (@svovaf)
2646 * @since 2.4.3
2647 *
2648 * @return bool
2649 */
2650 static function is_deactivation_snoozed() {
2651 $is_snoozed = ( ! is_multisite() || fs_is_network_admin() ) ?
2652 get_transient( 'fs_snooze_period' ) :
2653 get_site_transient( 'fs_snooze_period' );
2654
2655
2656 return ( 'true' === $is_snoozed );
2657 }
2658
2659 /**
2660 * Reset deactivation snoozing. When `$period` is `0` will stop deactivation snoozing by deleting the transients. Otherwise, will set the transients for the selected period.
2661 *
2662 * @param int $period Period in seconds.
2663 *
2664 * @author Vova Feldman (@svovaf)
2665 * @since 2.4.3
2666 */
2667 private static function reset_deactivation_snoozing( $period = 0 ) {
2668 $value = ( 0 === $period ) ? null : 'true';
2669
2670 if ( ! is_multisite() || fs_is_network_admin() ) {
2671 return set_transient( 'fs_snooze_period', $value, $period );
2672 } else {
2673 return set_site_transient( 'fs_snooze_period', $value, $period );
2674 }
2675 }
2676
2677 /**
2678 * The deactivation snooze expiration UNIX timestamp (in sec).
2679 *
2680 * @author Vova Feldman (@svovaf)
2681 * @since 2.4.3
2682 *
2683 * @return int
2684 */
2685 static function deactivation_snooze_expires_at() {
2686 return ( ! is_multisite() || fs_is_network_admin() ) ?
2687 (int) get_option( '_transient_timeout_fs_snooze_period' ) :
2688 (int) get_site_option( '_site_transient_timeout_fs_snooze_period' );
2689 }
2690
2691 #endregion
2692
2693 /**
2694 * @author Leo Fajardo (@leorw)
2695 * @since 2.1.4
2696 */
2697 function cancel_subscription_or_trial_ajax_action() {
2698 $this->_logger->entrance();
2699
2700 $this->check_ajax_referer( 'cancel_subscription_or_trial' );
2701
2702 $result = $this->cancel_subscription_or_trial( fs_request_get( 'plugin_id', $this->get_id() ), false );
2703
2704 if ( $this->is_api_error( $result ) ) {
2705 $this->shoot_ajax_failure( $result->error->message );
2706 }
2707
2708 $this->shoot_ajax_success();
2709 }
2710
2711 /**
2712 * @author Leo Fajardo (@leorw)
2713 * @since 2.1.4
2714 *
2715 * @param number $plugin_id
2716 *
2717 * @return object
2718 */
2719 private function cancel_subscription_or_trial( $plugin_id ) {
2720 $fs = null;
2721 if ( $plugin_id == $this->get_id() ) {
2722 $fs = $this;
2723 } else if ( $this->is_addon_activated( $plugin_id ) ) {
2724 $fs = self::get_instance_by_id( $plugin_id );
2725 }
2726
2727 $result = null;
2728
2729 if ( ! is_null( $fs ) ) {
2730 $result = $fs->is_paid_trial() ?
2731 $fs->_cancel_trial() :
2732 $fs->_downgrade_site();
2733 }
2734
2735 return $result;
2736 }
2737
2738 /**
2739 * @author Leo Fajardo (@leorw)
2740 * @since 2.0.2
2741 */
2742 function _delete_theme_update_data_action() {
2743 FS_Plugin_Updater::instance( $this )->delete_update_data();
2744 }
2745
2746 #endregion
2747
2748 #----------------------------------------------------------------------------------
2749 #region Instance
2750 #----------------------------------------------------------------------------------
2751
2752 /**
2753 * Main singleton instance.
2754 *
2755 * @author Vova Feldman (@svovaf)
2756 * @since 1.0.0
2757 *
2758 * @param number $module_id
2759 * @param string|bool $slug
2760 * @param bool $is_init Is initiation sequence.
2761 *
2762 * @return Freemius|false
2763 */
2764 static function instance( $module_id, $slug = false, $is_init = false ) {
2765 if ( empty( $module_id ) ) {
2766 return false;
2767 }
2768
2769 /**
2770 * 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.
2771 */
2772 self::_load_required_static();
2773
2774 if ( ! is_numeric( $module_id ) ) {
2775 if ( ! $is_init && true === $slug ) {
2776 $is_init = true;
2777 }
2778
2779 $slug = $module_id;
2780
2781 $module = FS_Plugin_Manager::instance( $slug )->get();
2782
2783 if ( is_object( $module ) ) {
2784 $module_id = $module->id;
2785 }
2786 }
2787
2788 $key = 'm_' . $module_id;
2789
2790 if ( ! isset( self::$_instances[ $key ] ) ) {
2791 self::$_instances[ $key ] = new Freemius( $module_id, $slug, $is_init );
2792 }
2793
2794 return self::$_instances[ $key ];
2795 }
2796
2797 /**
2798 * @author Vova Feldman (@svovaf)
2799 * @since 1.0.6
2800 *
2801 * @param number $addon_id
2802 *
2803 * @return bool
2804 */
2805 private static function has_instance( $addon_id ) {
2806 return isset( self::$_instances[ 'm_' . $addon_id ] );
2807 }
2808
2809 /**
2810 * @author Leo Fajardo (@leorw)
2811 * @since 1.2.2
2812 *
2813 * @param string|number $id_or_slug
2814 * @param string $module_type
2815 *
2816 * @return number|false
2817 */
2818 private static function get_module_id( $id_or_slug, $module_type = WP_FS__MODULE_TYPE_PLUGIN ) {
2819 if ( is_numeric( $id_or_slug ) ) {
2820 return $id_or_slug;
2821 }
2822
2823 foreach ( self::$_instances as $instance ) {
2824 // Also check the module type since there can be a plugin and a theme with the same slug.
2825 if ( ( $module_type === $instance->get_module_type() ) && ( $id_or_slug === $instance->get_slug() ) ) {
2826 return $instance->get_id();
2827 }
2828 }
2829
2830 return false;
2831 }
2832
2833 /**
2834 * @author Vova Feldman (@svovaf)
2835 * @since 1.0.6
2836 *
2837 * @param number $id
2838 *
2839 * @return false|Freemius
2840 */
2841 static function get_instance_by_id( $id ) {
2842 return isset ( self::$_instances[ 'm_' . $id ] ) ?
2843 self::$_instances[ 'm_' . $id ] :
2844 false;
2845 }
2846
2847 /**
2848 *
2849 * @author Vova Feldman (@svovaf)
2850 * @since 1.0.1
2851 *
2852 * @param string $plugin_file
2853 * @param string $module_type
2854 *
2855 * @return false|Freemius
2856 */
2857 static function get_instance_by_file( $plugin_file, $module_type = WP_FS__MODULE_TYPE_PLUGIN ) {
2858 $slug = self::find_slug_by_basename( $plugin_file );
2859
2860 return ( false !== $slug ) ?
2861 self::instance( self::get_module_id( $slug, $module_type ) ) :
2862 false;
2863 }
2864
2865 /**
2866 * @author Vova Feldman (@svovaf)
2867 * @since 1.0.6
2868 *
2869 * @return false|Freemius
2870 */
2871 function get_parent_instance() {
2872 return self::get_instance_by_id( $this->_plugin->parent_plugin_id );
2873 }
2874
2875 /**
2876 * @author Vova Feldman (@svovaf)
2877 * @since 1.0.6
2878 *
2879 * @param string|number $id_or_slug
2880 *
2881 * @return false|Freemius
2882 */
2883 function get_addon_instance( $id_or_slug ) {
2884 $addon_id = self::get_module_id( $id_or_slug );
2885
2886 return self::instance( $addon_id );
2887 }
2888
2889 /**
2890 * @return Freemius[]
2891 */
2892 static function _get_all_instances() {
2893 return self::$_instances;
2894 }
2895
2896 #endregion ------------------------------------------------------------------
2897
2898 /**
2899 * @author Vova Feldman (@svovaf)
2900 * @since 1.0.6
2901 *
2902 * @return bool
2903 */
2904 function is_parent_plugin_installed() {
2905 $is_active = self::has_instance( $this->_plugin->parent_plugin_id );
2906
2907 if ( $is_active ) {
2908 return true;
2909 }
2910
2911 /**
2912 * Parent module might be a theme. If that's the case, the add-on's FS
2913 * instance will be loaded prior to the theme's FS instance, therefore,
2914 * we need to check if it's active with a "look ahead".
2915 *
2916 * @author Vova Feldman
2917 * @since 1.2.2.3
2918 */
2919 global $fs_active_plugins;
2920 if ( is_object( $fs_active_plugins ) && is_array( $fs_active_plugins->plugins ) ) {
2921 $active_theme = wp_get_theme();
2922
2923 foreach ( $fs_active_plugins->plugins as $sdk => $module ) {
2924 if ( WP_FS__MODULE_TYPE_THEME === $module->type ) {
2925 if ( $module->plugin_path == $active_theme->get_stylesheet() ) {
2926 // Parent module is a theme and it's currently active.
2927 return true;
2928 }
2929 }
2930 }
2931 }
2932
2933 return false;
2934 }
2935
2936 /**
2937 * Check if add-on parent plugin in activation mode.
2938 *
2939 * @author Vova Feldman (@svovaf)
2940 * @since 1.0.7
2941 *
2942 * @return bool
2943 */
2944 function is_parent_in_activation() {
2945 $parent_fs = $this->get_parent_instance();
2946 if ( ! is_object( $parent_fs ) ) {
2947 return false;
2948 }
2949
2950 return ( $parent_fs->is_activation_mode() );
2951 }
2952
2953 /**
2954 * Is plugin in activation mode.
2955 *
2956 * @author Vova Feldman (@svovaf)
2957 * @since 1.0.7
2958 *
2959 * @param bool $and_on
2960 *
2961 * @return bool
2962 */
2963 function is_activation_mode( $and_on = true ) {
2964 return fs_is_network_admin() ?
2965 $this->is_network_activation_mode( $and_on ) :
2966 $this->is_site_activation_mode( $and_on );
2967 }
2968
2969 /**
2970 * Is plugin in activation mode.
2971 *
2972 * @author Vova Feldman (@svovaf)
2973 * @since 1.0.7
2974 *
2975 * @param bool $and_on
2976 *
2977 * @return bool
2978 */
2979 function is_site_activation_mode( $and_on = true ) {
2980 return (
2981 ( $this->is_on() || ! $and_on ) &&
2982 (
2983 ( $this->is_premium() && true === $this->_storage->require_license_activation ) ||
2984 (
2985 ( ! $this->is_registered() ||
2986 ( $this->is_only_premium() && ! $this->has_features_enabled_license() ) ) &&
2987 ( ! $this->is_enable_anonymous() ||
2988 ( ! $this->is_anonymous() && ! $this->is_pending_activation() ) )
2989 )
2990 )
2991 );
2992 }
2993
2994 /**
2995 * Checks if the SDK in network activation mode.
2996 *
2997 * @author Leo Fajardo (@leorw)
2998 * @since 2.0.0
2999 *
3000 * @param bool $and_on
3001 *
3002 * @return bool
3003 */
3004 private function is_network_activation_mode( $and_on = true ) {
3005 if ( ! $this->_is_network_active ) {
3006 // Not network activated.
3007 return false;
3008 }
3009
3010 if ( $this->is_network_upgrade_mode() ) {
3011 // Special flag to enforce network activation mode to decide what to do with the sites that are not yet opted-in nor skipped.
3012 return true;
3013 }
3014
3015 if ( ! $this->is_site_activation_mode( $and_on ) ) {
3016 // 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.
3017 return false;
3018 }
3019
3020 if ( $this->is_network_delegated_connection() ) {
3021 // Super-admin delegated the connection to the site admins -> not activation mode.
3022 return false;
3023 }
3024
3025 if ( $this->is_network_anonymous() && true !== $this->_storage->require_license_activation ) {
3026 // Super-admin skipped the connection network wide -> not activation mode.
3027 return false;
3028 }
3029
3030 if ( $this->is_network_registered() ) {
3031 // Super-admin connected at least one site -> not activation mode.
3032 return false;
3033 }
3034
3035 return true;
3036 }
3037
3038 /**
3039 * Check if current page is the opt-in/pending-activation page.
3040 *
3041 * @author Vova Feldman (@svovaf)
3042 * @since 1.2.1.7
3043 *
3044 * @return bool
3045 */
3046 function is_activation_page() {
3047 if ( $this->_menu->is_activation_page( $this->show_opt_in_on_themes_page() ) ) {
3048 return true;
3049 }
3050
3051 if ( ! $this->is_activation_mode() ) {
3052 return false;
3053 }
3054
3055 // Check if current page is matching the activation page.
3056 return $this->is_matching_url( $this->get_activation_url() );
3057 }
3058
3059 /**
3060 * Check if URL path's are matching and that all querystring
3061 * arguments of the $sub_url exist in the $url with the same values.
3062 *
3063 * WARNING:
3064 * 1. This method doesn't check if the sub/domain are matching.
3065 * 2. Ignore case sensitivity.
3066 *
3067 * @author Vova Feldman (@svovaf)
3068 * @since 1.2.1.7
3069 *
3070 * @param string $sub_url
3071 * @param string $url If argument is not set, check if the sub_url matching the current's page URL.
3072 *
3073 * @return bool
3074 */
3075 private function is_matching_url( $sub_url, $url = '' ) {
3076 if ( empty( $url ) ) {
3077 $url = $_SERVER['REQUEST_URI'];
3078 }
3079
3080 $url = strtolower( $url );
3081 $sub_url = strtolower( $sub_url );
3082
3083 if ( parse_url( $sub_url, PHP_URL_PATH ) !== parse_url( $url, PHP_URL_PATH ) ) {
3084 // Different path - DO NOT OVERRIDE PAGE.
3085 return false;
3086 }
3087
3088 $url_params = array();
3089 parse_str( parse_url( $url, PHP_URL_QUERY ), $url_params );
3090
3091 $sub_url_params = array();
3092 parse_str( parse_url( $sub_url, PHP_URL_QUERY ), $sub_url_params );
3093
3094 foreach ( $sub_url_params as $key => $val ) {
3095 if ( ! isset( $url_params[ $key ] ) || $val != $url_params[ $key ] ) {
3096 // Not matching query string - DO NOT OVERRIDE PAGE.
3097 return false;
3098 }
3099 }
3100
3101 return true;
3102 }
3103
3104 /**
3105 * Get the basenames of all active plugins for specific blog. Including network activated plugins.
3106 *
3107 * @author Vova Feldman (@svovaf)
3108 * @since 2.0.0
3109 *
3110 * @param int $blog_id
3111 *
3112 * @return string[]
3113 */
3114 private static function get_active_plugins_basenames( $blog_id = 0 ) {
3115 if ( is_multisite() && $blog_id > 0 ) {
3116 $active_basenames = get_blog_option( $blog_id, 'active_plugins' );
3117 } else {
3118 $active_basenames = get_option( 'active_plugins' );
3119 }
3120
3121 if ( ! is_array( $active_basenames ) ) {
3122 $active_basenames = array();
3123 }
3124
3125 if ( is_multisite() ) {
3126 $network_active_basenames = get_site_option( 'active_sitewide_plugins' );
3127
3128 if ( is_array( $network_active_basenames ) && ! empty( $network_active_basenames ) ) {
3129 $active_basenames = array_merge( $active_basenames, array_keys( $network_active_basenames ) );
3130 }
3131 }
3132
3133 return $active_basenames;
3134 }
3135
3136 /**
3137 * @author Leo Fajardo (@leorw)
3138 * @since 2.3.0
3139 *
3140 * @param int $blog_id
3141 *
3142 * @return array
3143 */
3144 static function get_active_plugins_directories_map( $blog_id = 0 ) {
3145 $active_basenames = self::get_active_plugins_basenames( $blog_id );
3146
3147 $map = array();
3148
3149 foreach ( $active_basenames as $active_basename ) {
3150 $active_basename = fs_normalize_path( $active_basename );
3151
3152 if ( false === strpos( $active_basename, '/' ) ) {
3153 continue;
3154 }
3155
3156 $map[ dirname( $active_basename ) ] = true;
3157 }
3158
3159 return $map;
3160 }
3161
3162 /**
3163 * Get collection of all active plugins. Including network activated plugins.
3164 *
3165 * @author Vova Feldman (@svovaf)
3166 * @since 1.0.9
3167 *
3168 * @param int $blog_id Since 2.0.0
3169 *
3170 * @return array[string]array
3171 */
3172 private static function get_active_plugins( $blog_id = 0 ) {
3173 self::require_plugin_essentials();
3174
3175 $active_plugin = array();
3176 $all_plugins = fs_get_plugins();
3177 $active_plugins_basenames = self::get_active_plugins_basenames( $blog_id );
3178
3179 foreach ( $active_plugins_basenames as $plugin_basename ) {
3180 $active_plugin[ $plugin_basename ] = $all_plugins[ $plugin_basename ];
3181 }
3182
3183 return $active_plugin;
3184 }
3185
3186 /**
3187 * Get collection of all site active plugins for a specified blog.
3188 *
3189 * @author Vova Feldman (@svovaf)
3190 * @since 2.0.0
3191 *
3192 * @param int $blog_id
3193 *
3194 * @return array[string]array
3195 */
3196 private static function get_site_active_plugins( $blog_id = 0 ) {
3197 $active_basenames = ( is_multisite() && $blog_id > 0 ) ?
3198 get_blog_option( $blog_id, 'active_plugins' ) :
3199 get_option( 'active_plugins' );
3200
3201 $active = array();
3202
3203 if ( ! is_array( $active_basenames ) ) {
3204 return $active;
3205 }
3206
3207 foreach ( $active_basenames as $basename ) {
3208 $active[ $basename ] = array(
3209 'is_active' => true,
3210 'Version' => '1.0', // Dummy version.
3211 'slug' => self::get_plugin_slug( $basename ),
3212 );
3213 }
3214
3215 return $active;
3216 }
3217
3218 /**
3219 * Get collection of all plugins with their activation status for a specified blog.
3220 *
3221 * @author Vova Feldman (@svovaf)
3222 * @since 1.1.8
3223 *
3224 * @param int $blog_id Since 2.0.0
3225 *
3226 * @return array Key is the plugin file path and the value is an array of the plugin data.
3227 */
3228 private static function get_all_plugins( $blog_id = 0 ) {
3229 self::require_plugin_essentials();
3230
3231 $all_plugins = fs_get_plugins();
3232
3233 $active_plugins_basenames = self::get_active_plugins_basenames( $blog_id );
3234
3235 foreach ( $all_plugins as $basename => &$data ) {
3236 // By default set to inactive (next foreach update the active plugins).
3237 $data['is_active'] = false;
3238 // Enrich with plugin slug.
3239 $data['slug'] = self::get_plugin_slug( $basename );
3240 }
3241
3242 // Flag active plugins.
3243 foreach ( $active_plugins_basenames as $basename ) {
3244 if ( isset( $all_plugins[ $basename ] ) ) {
3245 $all_plugins[ $basename ]['is_active'] = true;
3246 }
3247 }
3248
3249 return $all_plugins;
3250 }
3251
3252 /**
3253 * Get collection of all plugins and if they are network level activated.
3254 *
3255 * @author Vova Feldman (@svovaf)
3256 * @since 2.0.0
3257 *
3258 * @return array Key is the plugin basename and the value is an array of the plugin data.
3259 */
3260 private static function get_network_plugins() {
3261 self::require_plugin_essentials();
3262
3263 $all_plugins = fs_get_plugins();
3264
3265 $network_active_basenames = is_multisite() ?
3266 get_site_option( 'active_sitewide_plugins' ) :
3267 array();
3268
3269 foreach ( $all_plugins as $basename => &$data ) {
3270 // By default set to inactive (next foreach update the active plugins).
3271 $data['is_active'] = false;
3272 // Enrich with plugin slug.
3273 $data['slug'] = self::get_plugin_slug( $basename );
3274 }
3275
3276 // Flag active plugins.
3277 foreach ( $network_active_basenames as $basename ) {
3278 if ( isset( $all_plugins[ $basename ] ) ) {
3279 $all_plugins[ $basename ]['is_active'] = true;
3280 }
3281 }
3282
3283 return $all_plugins;
3284 }
3285
3286 /**
3287 * Cached result of get_site_transient( 'update_plugins' )
3288 *
3289 * @author Vova Feldman (@svovaf)
3290 * @since 1.1.8
3291 *
3292 * @var object
3293 */
3294 private static $_plugins_info;
3295
3296 /**
3297 * Helper function to get specified plugin's slug.
3298 *
3299 * @author Vova Feldman (@svovaf)
3300 * @since 1.1.8
3301 *
3302 * @param $basename
3303 *
3304 * @return string
3305 */
3306 private static function get_plugin_slug( $basename ) {
3307 if ( ! isset( self::$_plugins_info ) ) {
3308 self::$_plugins_info = get_site_transient( 'update_plugins' );
3309 }
3310
3311 $slug = '';
3312
3313 if ( is_object( self::$_plugins_info ) ) {
3314 if ( isset( self::$_plugins_info->no_update ) &&
3315 isset( self::$_plugins_info->no_update[ $basename ] ) &&
3316 ! empty( self::$_plugins_info->no_update[ $basename ]->slug )
3317 ) {
3318 $slug = self::$_plugins_info->no_update[ $basename ]->slug;
3319 } else if ( isset( self::$_plugins_info->response ) &&
3320 isset( self::$_plugins_info->response[ $basename ] ) &&
3321 ! empty( self::$_plugins_info->response[ $basename ]->slug )
3322 ) {
3323 $slug = self::$_plugins_info->response[ $basename ]->slug;
3324 }
3325 }
3326
3327 if ( empty( $slug ) ) {
3328 // Try to find slug from FS data.
3329 $slug = self::find_slug_by_basename( $basename );
3330 }
3331
3332 if ( empty( $slug ) ) {
3333 // Fallback to plugin's folder name.
3334 $slug = dirname( $basename );
3335 }
3336
3337 return $slug;
3338 }
3339
3340 private static $_statics_loaded = false;
3341
3342 /**
3343 * Load static resources.
3344 *
3345 * @author Vova Feldman (@svovaf)
3346 * @since 1.0.1
3347 */
3348 private static function _load_required_static() {
3349 if ( self::$_statics_loaded ) {
3350 return;
3351 }
3352
3353 self::$_static_logger = FS_Logger::get_logger( WP_FS__SLUG, WP_FS__DEBUG_SDK, WP_FS__ECHO_DEBUG_SDK );
3354
3355 self::$_static_logger->entrance();
3356
3357 self::$_accounts = FS_Options::instance( WP_FS__ACCOUNTS_OPTION_NAME, true );
3358
3359 if ( is_multisite() ) {
3360 $has_skipped_migration = (
3361 // 'id_slug_type_path_map' - was never stored on older versions, therefore, not exists on the site level.
3362 null === self::$_accounts->get_option( 'id_slug_type_path_map', null, false ) &&
3363 // 'file_slug_map' stored on the site level, so it was running an SDK version before it was integrated with MS-network.
3364 null !== self::$_accounts->get_option( 'file_slug_map', null, false )
3365 );
3366
3367 /**
3368 * If the file_slug_map exists on the site level but doesn't exist on the
3369 * network level storage, it means that we need to process the storage with migration.
3370 *
3371 * 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.
3372 *
3373 * @author Vova Feldman (@svovaf)
3374 * @since 2.0.0
3375 */
3376 if (
3377 ( $has_skipped_migration && true !== self::$_accounts->get_option( 'ms_migration_complete', false, true ) ) ||
3378 ( null === self::$_accounts->get_option( 'file_slug_map', null, true ) &&
3379 null !== self::$_accounts->get_option( 'file_slug_map', null, false ) )
3380 ) {
3381 self::migrate_options_to_network();
3382 }
3383 }
3384
3385 self::$_global_admin_notices = FS_Admin_Notices::instance( 'global' );
3386
3387 if ( ! WP_FS__DEMO_MODE ) {
3388 add_action( ( fs_is_network_admin() ? 'network_' : '' ) . 'admin_menu', array(
3389 'Freemius',
3390 '_add_debug_section'
3391 ) );
3392 }
3393
3394 add_action( "wp_ajax_fs_toggle_debug_mode", array( 'Freemius', '_toggle_debug_mode' ) );
3395
3396 self::add_ajax_action_static( 'get_debug_log', array( 'Freemius', '_get_debug_log' ) );
3397
3398 self::add_ajax_action_static( 'get_db_option', array( 'Freemius', '_get_db_option' ) );
3399
3400 self::add_ajax_action_static( 'set_db_option', array( 'Freemius', '_set_db_option' ) );
3401
3402 if ( 0 == did_action( 'plugins_loaded' ) ) {
3403 add_action( 'plugins_loaded', array( 'Freemius', '_load_textdomain' ), 1 );
3404 }
3405
3406 $clone_manager = FS_Clone_Manager::instance();
3407 add_action( 'init', array( $clone_manager, '_init' ) );
3408
3409 add_action( 'admin_footer', array( 'Freemius', '_open_support_forum_in_new_page' ) );
3410
3411 if ( self::is_plugins_page() || self::is_themes_page() ) {
3412 add_action( 'admin_print_footer_scripts', array( 'Freemius', '_maybe_add_beta_label_styles' ), 9 );
3413
3414 /**
3415 * Specifically use this hook so that the JS event handlers will work properly on the "Themes"
3416 * page.
3417 *
3418 * @author Leo Fajardo (@leorw)
3419 * @since 2.3.0
3420 */
3421 add_action( 'admin_footer-' . self::get_current_page(), array( 'Freemius', '_maybe_add_beta_label_to_plugins_and_handle_confirmation') );
3422 }
3423
3424 self::$_statics_loaded = true;
3425 }
3426
3427 #--------------------------------------------------------------------------------
3428 #region Clone
3429 #--------------------------------------------------------------------------------
3430
3431 /**
3432 * @author Leo Fajardo (@leorw)
3433 * @since 2.5.0
3434 *
3435 * @param bool $only_if_manual_resolution_is_not_hidden
3436 *
3437 * @return bool
3438 */
3439 private function is_unresolved_clone( $only_if_manual_resolution_is_not_hidden = false ) {
3440 if ( ! $this->is_clone( $only_if_manual_resolution_is_not_hidden ) ) {
3441 return false;
3442 }
3443
3444 return FS_Clone_Manager::instance()->has_temporary_duplicate_mode_expired();
3445 }
3446
3447 /**
3448 * @author Leo Fajardo (@leorw)
3449 * @since 2.5.0
3450 *
3451 * @param bool $only_if_manual_resolution_is_not_hidden
3452 */
3453 function is_clone( $only_if_manual_resolution_is_not_hidden = false ) {
3454 if ( ! is_object( $this->_site ) ) {
3455 return false;
3456 }
3457
3458 $blog_id = null;
3459
3460 if (
3461 fs_is_network_admin() &&
3462 FS_Site::is_valid_id( $this->_storage->network_install_blog_id )
3463 ) {
3464 // Ensure that we're comparing the network install's URL with the relevant subsite's URL.
3465 $blog_id = $this->_storage->network_install_blog_id;
3466 }
3467
3468 $site_url = Freemius::get_unfiltered_site_url( $blog_id, true, true );
3469
3470 if ( ! $this->_site->is_clone( $site_url ) ) {
3471 return false;
3472 }
3473
3474 return (
3475 ! $only_if_manual_resolution_is_not_hidden ||
3476 ! FS_Clone_Manager::instance()->should_hide_manual_resolution()
3477 );
3478 }
3479
3480 /**
3481 * @author Leo Fajardo (@leorw)
3482 * @since 2.5.0
3483 *
3484 * @param int|null $blog_id
3485 * @param bool $strip_protocol
3486 * @param bool $add_trailing_slash
3487 *
3488 * @return string
3489 */
3490 static function get_unfiltered_site_url( $blog_id = null, $strip_protocol = false, $add_trailing_slash = false ) {
3491 global $wp_filter;
3492
3493 $site_url_filters = array(
3494 'site_url' => null,
3495 'pre_option_siteurl' => null,
3496 'default_option_siteurl' => null,
3497 'option_siteurl' => null,
3498 );
3499
3500 // Detach all URL-related filters to get the actual site's URL (stripped of potential manipulations by multilingual plugins).
3501 foreach ( $site_url_filters as $hook_name => $site_url_filter ) {
3502 if ( ! empty( $wp_filter[ $hook_name ] ) ) {
3503 $site_url_filters[ $hook_name ] = $wp_filter[ $hook_name ];
3504 unset( $wp_filter[ $hook_name ] );
3505 }
3506 }
3507
3508 $url = get_site_url( $blog_id );
3509
3510 // Re-attach the filters back.
3511 foreach ( $site_url_filters as $hook_name => $site_url_filter ) {
3512 if ( ! empty( $site_url_filter ) ) {
3513 $wp_filter[ $hook_name ] = $site_url_filter;
3514 }
3515 }
3516
3517 if ( $strip_protocol ) {
3518 $url = fs_strip_url_protocol( $url );
3519 }
3520
3521 if ( $add_trailing_slash ) {
3522 $url = trailingslashit( $url );
3523 }
3524
3525 return $url;
3526 }
3527
3528 /**
3529 * @author Leo Fajardo (@leorw)
3530 * @since 2.5.0
3531 *
3532 * @param number $site_id
3533 */
3534 function fetch_install_by_id( $site_id ) {
3535 return $this->get_current_or_network_user_api_scope()->get( "/installs/{$site_id}.json" );
3536 }
3537
3538 /**
3539 * @author Leo Fajardo (@leorw)
3540 * @since 2.5.0
3541 *
3542 * @return string|object|bool
3543 */
3544 function _handle_long_term_duplicate() {
3545 $this->_logger->entrance();
3546
3547 $this->delete_current_install( false );
3548
3549 $license_key = false;
3550
3551 if (
3552 is_object( $this->_license ) &&
3553 ! $this->_license->is_utilized(
3554 ( WP_FS__IS_LOCALHOST_FOR_SERVER || FS_Site::is_localhost_by_address( self::get_unfiltered_site_url() ) )
3555 )
3556 ) {
3557 $license_key = $this->_license->secret_key;
3558 }
3559
3560 return $this->opt_in(
3561 false,
3562 false,
3563 false,
3564 $license_key,
3565 false,
3566 false,
3567 false,
3568 null,
3569 array(),
3570 false
3571 );
3572 }
3573
3574 #endregion
3575
3576 /**
3577 * @author Leo Fajardo (@leorw)
3578 *
3579 * @since 2.1.3
3580 */
3581 private static function migrate_options_to_network() {
3582 self::migrate_accounts_to_network();
3583
3584 // Migrate API options from site level to network level.
3585 $api_network_options = FS_Option_Manager::get_manager( WP_FS__OPTIONS_OPTION_NAME, true, true );
3586 $api_network_options->migrate_to_network();
3587
3588 // Migrate API cache to network level storage.
3589 FS_Cache_Manager::get_manager( WP_FS__API_CACHE_OPTION_NAME )->migrate_to_network();
3590
3591 self::$_accounts->set_option( 'ms_migration_complete', true, true );
3592 }
3593
3594 #----------------------------------------------------------------------------------
3595 #region Localization
3596 #----------------------------------------------------------------------------------
3597
3598 /**
3599 * Load framework's text domain.
3600 *
3601 * @author Vova Feldman (@svovaf)
3602 * @since 1.2.1
3603 */
3604 static function _load_textdomain() {
3605 if ( ! is_admin() ) {
3606 return;
3607 }
3608
3609 global $fs_active_plugins;
3610
3611 // Works both for plugins and themes.
3612 load_plugin_textdomain(
3613 'freemius',
3614 false,
3615 $fs_active_plugins->newest->sdk_path . '/languages/'
3616 );
3617 }
3618
3619 #endregion
3620
3621 #----------------------------------------------------------------------------------
3622 #region Debugging
3623 #----------------------------------------------------------------------------------
3624
3625 /**
3626 * @author Vova Feldman (@svovaf)
3627 * @since 1.0.8
3628 */
3629 static function _add_debug_section() {
3630 if ( ! is_super_admin() ) {
3631 // Add debug page only for super-admins.
3632 return;
3633 }
3634
3635 self::$_static_logger->entrance();
3636
3637 $title = sprintf( '%s [v.%s]', fs_text_inline( 'Freemius Debug' ), WP_FS__SDK_VERSION );
3638
3639 if ( WP_FS__DEV_MODE ) {
3640 // Add top-level debug menu item.
3641 $hook = FS_Admin_Menu_Manager::add_page(
3642 $title,
3643 $title,
3644 'manage_options',
3645 'freemius',
3646 array( 'Freemius', '_debug_page_render' )
3647 );
3648 } else {
3649 // Add hidden debug page.
3650 $hook = FS_Admin_Menu_Manager::add_subpage(
3651 '',
3652 $title,
3653 $title,
3654 'manage_options',
3655 'freemius',
3656 array( 'Freemius', '_debug_page_render' )
3657 );
3658 }
3659
3660 if ( ! empty( $hook ) ) {
3661 add_action( "load-$hook", array( 'Freemius', '_debug_page_actions' ) );
3662 }
3663 }
3664
3665 /**
3666 * @author Vova Feldman (@svovaf)
3667 * @since 1.1.7.3
3668 */
3669 static function _toggle_debug_mode() {
3670 check_admin_referer( 'fs_toggle_debug_mode' );
3671
3672 if ( ! is_super_admin() ) {
3673 return;
3674 }
3675
3676 $is_on = fs_request_get( 'is_on', false, 'post' );
3677
3678 if ( fs_request_is_post() && in_array( $is_on, array( 0, 1 ) ) ) {
3679 update_option( 'fs_debug_mode', $is_on );
3680
3681 // Turn on/off storage logging.
3682 FS_Logger::_set_storage_logging( ( 1 == $is_on ) );
3683 }
3684
3685 exit;
3686 }
3687
3688 /**
3689 * @author Vova Feldman (@svovaf)
3690 * @since 1.2.1.6
3691 */
3692 static function _get_debug_log() {
3693 check_admin_referer( 'fs_get_debug_log' );
3694
3695 if ( ! is_super_admin() ) {
3696 return;
3697 }
3698
3699 $limit = min( ! empty( $_POST['limit'] ) ? absint( $_POST['limit'] ) : 200, 200 );
3700 $offset = min( ! empty( $_POST['offset'] ) ? absint( $_POST['offset'] ) : 200, 200 );
3701
3702 $logs = FS_Logger::load_db_logs(
3703 fs_request_get( 'filters', false, 'post' ),
3704 $limit,
3705 $offset
3706 );
3707
3708 self::shoot_ajax_success( $logs );
3709 }
3710
3711 /**
3712 * @author Vova Feldman (@svovaf)
3713 * @since 1.2.1.7
3714 */
3715 static function _get_db_option() {
3716 check_admin_referer( 'fs_get_db_option' );
3717
3718 $option_name = fs_request_get( 'option_name' );
3719
3720 if ( ! is_super_admin() ||
3721 ! fs_starts_with( $option_name, 'fs_' )
3722 ) {
3723 self::shoot_ajax_failure();
3724 }
3725
3726 $value = get_option( $option_name );
3727
3728 $result = array(
3729 'name' => $option_name,
3730 );
3731
3732 if ( false !== $value ) {
3733 if ( ! is_string( $value ) ) {
3734 $value = json_encode( $value );
3735 }
3736
3737 $result['value'] = $value;
3738 }
3739
3740 self::shoot_ajax_success( $result );
3741 }
3742
3743 /**
3744 * @author Vova Feldman (@svovaf)
3745 * @since 1.2.1.7
3746 */
3747 static function _set_db_option() {
3748 check_admin_referer( 'fs_set_db_option' );
3749
3750 $option_name = fs_request_get( 'option_name' );
3751
3752 if ( ! is_super_admin() ||
3753 ! fs_starts_with( $option_name, 'fs_' )
3754 ) {
3755 self::shoot_ajax_failure();
3756 }
3757
3758 $option_value = fs_request_get( 'option_value' );
3759
3760 if ( ! empty( $option_value ) ) {
3761 update_option( $option_name, $option_value );
3762 }
3763
3764 self::shoot_ajax_success();
3765 }
3766
3767 /**
3768 * @author Vova Feldman (@svovaf)
3769 * @since 1.0.8
3770 */
3771 static function _debug_page_actions() {
3772 self::_clean_admin_content_section();
3773
3774 if ( fs_request_is_action( 'restart_freemius' ) ) {
3775 check_admin_referer( 'restart_freemius' );
3776
3777 if ( ! is_multisite() ) {
3778 // Clear accounts data.
3779 self::$_accounts->clear( null, true );
3780 } else {
3781 $sites = self::get_sites();
3782 foreach ( $sites as $site ) {
3783 $blog_id = self::get_site_blog_id( $site );
3784 self::$_accounts->clear( $blog_id, true );
3785 }
3786
3787 // Clear network level storage.
3788 self::$_accounts->clear( true, true );
3789 }
3790
3791 // Clear SDK reference cache.
3792 delete_option( 'fs_active_plugins' );
3793 } else if ( fs_request_is_action( 'clear_updates_data' ) ) {
3794 check_admin_referer( 'clear_updates_data' );
3795
3796 if ( ! is_multisite() ) {
3797 set_site_transient( 'update_plugins', null );
3798 set_site_transient( 'update_themes', null );
3799 } else {
3800 $current_blog_id = get_current_blog_id();
3801
3802 $sites = self::get_sites();
3803 foreach ( $sites as $site ) {
3804 switch_to_blog( self::get_site_blog_id( $site ) );
3805
3806 set_site_transient( 'update_plugins', null );
3807 set_site_transient( 'update_themes', null );
3808 }
3809
3810 switch_to_blog( $current_blog_id );
3811 }
3812 } else if ( fs_request_is_action( 'reset_deactivation_snoozing' ) ) {
3813 check_admin_referer( 'reset_deactivation_snoozing' );
3814
3815 self::reset_deactivation_snoozing();
3816 } else if ( fs_request_is_action( 'simulate_trial' ) ) {
3817 check_admin_referer( 'simulate_trial' );
3818
3819 $fs = freemius( fs_request_get( 'module_id' ) );
3820
3821 // Update SDK install to at least 24 hours before.
3822 $fs->_storage->install_timestamp = ( time() - WP_FS__TIME_24_HOURS_IN_SEC );
3823 // Unset the trial shown timestamp.
3824 unset( $fs->_storage->trial_promotion_shown );
3825 } else if ( fs_request_is_action( 'simulate_network_upgrade' ) ) {
3826 check_admin_referer( 'simulate_network_upgrade' );
3827
3828 $fs = freemius( fs_request_get( 'module_id' ) );
3829
3830 self::set_network_upgrade_mode( $fs->_storage );
3831 } else if ( fs_request_is_action( 'delete_install' ) ) {
3832 check_admin_referer( 'delete_install' );
3833
3834 self::_delete_site_by_slug(
3835 fs_request_get( 'slug' ),
3836 fs_request_get( 'module_type' ),
3837 true,
3838 fs_request_get( 'blog_id', null )
3839 );
3840 } else if ( fs_request_is_action( 'delete_user' ) ) {
3841 check_admin_referer( 'delete_user' );
3842
3843 self::delete_user( fs_request_get( 'user_id' ) );
3844 } else if ( fs_request_is_action( 'download_logs' ) ) {
3845 check_admin_referer( 'download_logs' );
3846
3847 $download_url = FS_Logger::download_db_logs(
3848 fs_request_get( 'filters', false, 'post' )
3849 );
3850
3851 if ( false === $download_url ) {
3852 wp_die( 'Oops... there was an error while generating the logs download file. Please try again and if it doesn\'t work contact support@freemius.com.' );
3853 }
3854
3855 fs_redirect( $download_url );
3856 } else if ( fs_request_is_action( 'migrate_options_to_network' ) ) {
3857 check_admin_referer( 'migrate_options_to_network' );
3858
3859 self::migrate_options_to_network();
3860 }
3861 }
3862
3863 /**
3864 * @author Leo Fajardo (@leorw)
3865 * @since 2.5.0
3866 *
3867 * @return array
3868 */
3869 static function get_all_modules_sites() {
3870 self::$_static_logger->entrance();
3871
3872 $sites_by_type = array(
3873 WP_FS__MODULE_TYPE_PLUGIN => array(),
3874 WP_FS__MODULE_TYPE_THEME => array(),
3875 );
3876
3877 $module_types = array_keys( $sites_by_type );
3878
3879 if ( ! is_multisite() ) {
3880 foreach ( $module_types as $type ) {
3881 $sites_by_type[ $type ] = self::get_all_sites( $type );
3882
3883 foreach ( $sites_by_type[ $type ] as $slug => $install ) {
3884 $sites_by_type[ $type ][ $slug ] = array( $install );
3885 }
3886 }
3887 } else {
3888 $sites = self::get_sites();
3889
3890 foreach ( $sites as $site ) {
3891 $blog_id = self::get_site_blog_id( $site );
3892
3893 foreach ( $module_types as $type ) {
3894 $installs = self::get_all_sites( $type, $blog_id );
3895
3896 foreach ( $installs as $slug => $install ) {
3897 if ( ! isset( $sites_by_type[ $type ][ $slug ] ) ) {
3898 $sites_by_type[ $type ][ $slug ] = array();
3899 }
3900
3901 $install->blog_id = $blog_id;
3902
3903 $sites_by_type[ $type ][ $slug ][] = $install;
3904 }
3905
3906 }
3907 }
3908 }
3909
3910 return $sites_by_type;
3911 }
3912
3913 /**
3914 * @author Vova Feldman (@svovaf)
3915 * @since 1.0.8
3916 */
3917 static function _debug_page_render() {
3918 self::$_static_logger->entrance();
3919
3920 $all_modules_sites = self::get_all_modules_sites();
3921
3922 $licenses_by_module_type = self::get_all_licenses_by_module_type();
3923
3924 $vars = array(
3925 'plugin_sites' => $all_modules_sites[ WP_FS__MODULE_TYPE_PLUGIN ],
3926 'theme_sites' => $all_modules_sites[ WP_FS__MODULE_TYPE_THEME ],
3927 'users' => self::get_all_users(),
3928 'addons' => self::get_all_addons(),
3929 'account_addons' => self::get_all_account_addons(),
3930 'plugin_licenses' => $licenses_by_module_type[ WP_FS__MODULE_TYPE_PLUGIN ],
3931 'theme_licenses' => $licenses_by_module_type[ WP_FS__MODULE_TYPE_THEME ]
3932 );
3933
3934 fs_enqueue_local_style( 'fs_debug', '/admin/debug.css' );
3935 fs_require_once_template( 'debug.php', $vars );
3936 }
3937
3938 #endregion
3939
3940 #----------------------------------------------------------------------------------
3941 #region Connectivity Issues
3942 #----------------------------------------------------------------------------------
3943
3944 /**
3945 * Check if Freemius should be turned on for the current plugin install.
3946 *
3947 * Note:
3948 * $this->_is_on is updated in has_api_connectivity()
3949 *
3950 * @author Vova Feldman (@svovaf)
3951 * @since 1.0.9
3952 *
3953 * @return bool
3954 */
3955 function is_on() {
3956 self::$_static_logger->entrance();
3957
3958 if ( is_object( $this->_site ) && ! $this->is_registered() ) {
3959 return false;
3960 }
3961
3962 if ( isset( $this->_is_on ) ) {
3963 return $this->_is_on;
3964 }
3965
3966 // If already installed or pending then sure it's on :)
3967 if ( $this->is_registered() || $this->is_pending_activation() ) {
3968 $this->_is_on = true;
3969
3970 return true;
3971 }
3972
3973 return false;
3974 }
3975
3976 /**
3977 * @author Vova Feldman (@svovaf)
3978 * @since 1.1.7.3
3979 *
3980 * @param bool $flush_if_no_connectivity
3981 *
3982 * @return bool
3983 */
3984 private function should_run_connectivity_test( $flush_if_no_connectivity = false ) {
3985 if ( ! isset( $this->_storage->connectivity_test ) ) {
3986 // Connectivity test was never executed, or cache was cleared.
3987 return true;
3988 }
3989
3990 if ( WP_FS__PING_API_ON_IP_OR_HOST_CHANGES ) {
3991 if ( WP_FS__IS_HTTP_REQUEST ) {
3992 if ( $_SERVER['HTTP_HOST'] != $this->_storage->connectivity_test['host'] ) {
3993 // Domain changed.
3994 return true;
3995 }
3996
3997 if ( WP_FS__REMOTE_ADDR != $this->_storage->connectivity_test['server_ip'] ) {
3998 // Server IP changed.
3999 return true;
4000 }
4001 }
4002 }
4003
4004 if ( $this->_storage->connectivity_test['is_connected'] &&
4005 $this->_storage->connectivity_test['is_active']
4006 ) {
4007 // API connected and Freemius is active - no need to run connectivity check.
4008 return false;
4009 }
4010
4011 if ( $flush_if_no_connectivity ) {
4012 /**
4013 * If explicitly asked to flush when no connectivity - do it only
4014 * if at least 10 sec passed from the last API connectivity test.
4015 */
4016 return ( isset( $this->_storage->connectivity_test['timestamp'] ) &&
4017 ( WP_FS__SCRIPT_START_TIME - $this->_storage->connectivity_test['timestamp'] ) > 10 );
4018 }
4019
4020 /**
4021 * @since 1.1.7 Don't check for connectivity on plugin downgrade.
4022 */
4023 $version = $this->get_plugin_version();
4024 if ( version_compare( $version, $this->_storage->connectivity_test['version'], '>' ) ) {
4025 // If it's a plugin version upgrade and Freemius is off or no connectivity, run connectivity test.
4026 return true;
4027 }
4028
4029 return false;
4030 }
4031
4032 /**
4033 * @author Vova Feldman (@svovaf)
4034 * @since 1.1.7.4
4035 *
4036 * @param int|null $blog_id Since 2.0.0.
4037 * @param bool $is_gdpr_test Since 2.0.2. Perform only the GDPR test.
4038 *
4039 * @return object|false
4040 */
4041 private function ping( $blog_id = null, $is_gdpr_test = false ) {
4042 if ( WP_FS__SIMULATE_NO_API_CONNECTIVITY ) {
4043 return false;
4044 }
4045
4046 $version = $this->get_plugin_version();
4047
4048 $is_update = $this->apply_filters( 'is_plugin_update', $this->is_plugin_update() );
4049
4050 $params = array(
4051 'is_update' => json_encode( $is_update ),
4052 'version' => $version,
4053 'sdk' => $this->version,
4054 'is_admin' => json_encode( is_admin() ),
4055 'is_ajax' => json_encode( self::is_ajax() ),
4056 'is_cron' => json_encode( self::is_cron() ),
4057 'is_gdpr_test' => $is_gdpr_test,
4058 'is_http' => json_encode( WP_FS__IS_HTTP_REQUEST ),
4059 );
4060
4061 if ( is_multisite() && function_exists( 'get_network' ) ) {
4062 $params['network_uid'] = $this->get_anonymous_network_id();
4063 }
4064
4065 return $this->get_api_plugin_scope()->ping(
4066 $this->get_anonymous_id( $blog_id ),
4067 $params
4068 );
4069 }
4070
4071 /**
4072 * Check if there's any connectivity issue to Freemius API.
4073 *
4074 * @author Vova Feldman (@svovaf)
4075 * @since 1.0.9
4076 *
4077 * @param bool $flush_if_no_connectivity
4078 *
4079 * @return bool
4080 */
4081 function has_api_connectivity( $flush_if_no_connectivity = false ) {
4082 $this->_logger->entrance();
4083
4084 if ( isset( $this->_has_api_connection ) && ( $this->_has_api_connection || ! $flush_if_no_connectivity ) ) {
4085 return $this->_has_api_connection;
4086 }
4087
4088 if ( WP_FS__SIMULATE_NO_API_CONNECTIVITY &&
4089 isset( $this->_storage->connectivity_test ) &&
4090 true === $this->_storage->connectivity_test['is_connected']
4091 ) {
4092 unset( $this->_storage->connectivity_test );
4093 }
4094
4095 if ( ! $this->should_run_connectivity_test( $flush_if_no_connectivity ) ) {
4096 $this->_has_api_connection = $this->_storage->connectivity_test['is_connected'];
4097 /**
4098 * @since 1.1.6 During dev mode, if there's connectivity - turn Freemius on regardless the configuration.
4099 *
4100 * @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.
4101 */
4102 $this->_is_on = $this->_storage->connectivity_test['is_active'] ||
4103 $this->is_premium() ||
4104 ( WP_FS__DEV_MODE && $this->_has_api_connection && ! WP_FS__SIMULATE_FREEMIUS_OFF );
4105
4106 return $this->_has_api_connection;
4107 }
4108
4109 $pong = $this->ping();
4110 $is_connected = $this->get_api_plugin_scope()->is_valid_ping( $pong );
4111
4112 if ( ! $is_connected ) {
4113 // API failure.
4114 $this->_add_connectivity_issue_message( $pong );
4115 }
4116
4117 if ( $is_connected ) {
4118 FS_GDPR_Manager::instance()->store_is_required( $pong->is_gdpr_required );
4119 }
4120
4121 $this->store_connectivity_info( $pong, $is_connected );
4122
4123 return $this->_has_api_connection;
4124 }
4125
4126 /**
4127 * @author Vova Feldman (@svovaf)
4128 * @since 1.1.7.4
4129 *
4130 * @param object $pong
4131 * @param bool $is_connected
4132 */
4133 private function store_connectivity_info( $pong, $is_connected ) {
4134 $this->_logger->entrance();
4135
4136 $version = $this->get_plugin_version();
4137
4138 if ( ! $is_connected || WP_FS__SIMULATE_FREEMIUS_OFF ) {
4139 $is_active = false;
4140 } else {
4141 $is_active = ( isset( $pong->is_active ) && true == $pong->is_active );
4142 }
4143
4144 $is_active = $this->apply_filters(
4145 'is_on',
4146 $is_active,
4147 $this->is_plugin_update(),
4148 $version
4149 );
4150
4151 $this->_storage->connectivity_test = array(
4152 'is_connected' => $is_connected,
4153 'host' => $_SERVER['HTTP_HOST'],
4154 'server_ip' => WP_FS__REMOTE_ADDR,
4155 'is_active' => $is_active,
4156 'timestamp' => WP_FS__SCRIPT_START_TIME,
4157 // Last version with connectivity attempt.
4158 'version' => $version,
4159 );
4160
4161 $this->_has_api_connection = $is_connected;
4162 $this->_is_on = $is_active || ( WP_FS__DEV_MODE && $is_connected && ! WP_FS__SIMULATE_FREEMIUS_OFF );
4163 }
4164
4165 /**
4166 * Force turning Freemius on.
4167 *
4168 * @author Vova Feldman (@svovaf)
4169 * @since 1.1.8.1
4170 *
4171 * @return bool TRUE if successfully turned on.
4172 */
4173 private function turn_on() {
4174 $this->_logger->entrance();
4175
4176 if ( $this->is_on() || ! isset( $this->_storage->connectivity_test['is_active'] ) ) {
4177 return false;
4178 }
4179
4180 $updated_connectivity = $this->_storage->connectivity_test;
4181 $updated_connectivity['is_active'] = true;
4182 $updated_connectivity['timestamp'] = WP_FS__SCRIPT_START_TIME;
4183 $this->_storage->connectivity_test = $updated_connectivity;
4184
4185 $this->_is_on = true;
4186
4187 return true;
4188 }
4189
4190 /**
4191 * Anonymous and unique site identifier (Hash).
4192 *
4193 * @author Vova Feldman (@svovaf)
4194 * @since 1.1.0
4195 *
4196 * @param null|int $blog_id Since 2.0.0
4197 *
4198 * @return string
4199 */
4200 function get_anonymous_id( $blog_id = null ) {
4201 $unique_id = self::$_accounts->get_option( 'unique_id', null, $blog_id );
4202
4203 if ( empty( $unique_id ) || ! is_string( $unique_id ) ) {
4204 $key = self::get_unfiltered_site_url( $blog_id, true );
4205
4206 $secure_auth = defined( 'SECURE_AUTH_KEY' ) ? SECURE_AUTH_KEY : '';
4207 if ( empty( $secure_auth ) ||
4208 false !== strpos( $secure_auth, ' ' ) ||
4209 'put your unique phrase here' === $secure_auth
4210 ) {
4211 // Protect against default auth key.
4212 $secure_auth = md5( microtime() );
4213 }
4214
4215 /**
4216 * Base the unique identifier on the WP secure authentication key. Which
4217 * turns the key into a secret anonymous identifier. This will help us
4218 * to avoid duplicate installs generation on the backend upon opt-in.
4219 *
4220 * @author Vova Feldman (@svovaf)
4221 * @since 1.2.3
4222 */
4223 $unique_id = md5( $key . $secure_auth );
4224
4225 self::$_accounts->set_option( 'unique_id', $unique_id, true, $blog_id );
4226 }
4227
4228 $this->_logger->departure( $unique_id );
4229
4230 return $unique_id;
4231 }
4232
4233 /**
4234 * Returns anonymous network ID.
4235 *
4236 * @since 2.4.3
4237 *
4238 * @return string
4239 */
4240 function get_anonymous_network_id() {
4241 return $this->get_anonymous_id( get_network()->site_id );
4242 }
4243
4244 /**
4245 * @author Vova Feldman (@svovaf)
4246 * @since 1.1.7.4
4247 *
4248 * @return \WP_User
4249 */
4250 static function _get_current_wp_user() {
4251 self::require_pluggable_essentials();
4252 self::wp_cookie_constants();
4253
4254 return wp_get_current_user();
4255 }
4256
4257 /**
4258 * Define cookie constants which are required by Freemius::_get_current_wp_user() since
4259 * it uses wp_get_current_user() which needs the cookie constants set. When a plugin
4260 * is network activated the cookie constants are only configured after the network
4261 * plugins activation, therefore, if we don't define those constants WP will throw
4262 * PHP warnings/notices.
4263 *
4264 * @author Vova Feldman (@svovaf)
4265 * @since 2.1.1
4266 */
4267 private static function wp_cookie_constants() {
4268 if ( defined( 'LOGGED_IN_COOKIE' ) &&
4269 ( defined( 'AUTH_COOKIE' ) || defined( 'SECURE_AUTH_COOKIE' ) )
4270 ) {
4271 return;
4272 }
4273
4274 /**
4275 * Used to guarantee unique hash cookies
4276 *
4277 * @since 1.5.0
4278 */
4279 if ( ! defined( 'COOKIEHASH' ) ) {
4280 $siteurl = get_site_option( 'siteurl' );
4281 if ( $siteurl ) {
4282 define( 'COOKIEHASH', md5( $siteurl ) );
4283 } else {
4284 define( 'COOKIEHASH', '' );
4285 }
4286 }
4287
4288 if ( ! defined( 'LOGGED_IN_COOKIE' ) ) {
4289 define( 'LOGGED_IN_COOKIE', 'wordpress_logged_in_' . COOKIEHASH );
4290 }
4291
4292 /**
4293 * @since 2.5.0
4294 */
4295 if ( ! defined( 'AUTH_COOKIE' ) ) {
4296 define( 'AUTH_COOKIE', 'wordpress_' . COOKIEHASH );
4297 }
4298
4299 /**
4300 * @since 2.6.0
4301 */
4302 if ( ! defined( 'SECURE_AUTH_COOKIE' ) ) {
4303 define( 'SECURE_AUTH_COOKIE', 'wordpress_sec_' . COOKIEHASH );
4304 }
4305 }
4306
4307 /**
4308 * @author Vova Feldman (@svovaf)
4309 * @since 2.1.0
4310 *
4311 * @return int
4312 */
4313 static function get_current_wp_user_id() {
4314 $wp_user = self::_get_current_wp_user();
4315
4316 return $wp_user->ID;
4317 }
4318
4319 /**
4320 * @author Vova Feldman (@svovaf)
4321 * @since 1.2.1.7
4322 *
4323 * @param string $email
4324 *
4325 * @return bool
4326 */
4327 static function is_valid_email( $email ) {
4328 if ( false === filter_var( $email, FILTER_VALIDATE_EMAIL ) ) {
4329 return false;
4330 }
4331
4332 $parts = explode( '@', $email );
4333
4334 if ( 2 !== count( $parts ) || empty( $parts[1] ) ) {
4335 return false;
4336 }
4337
4338 $blacklist = array(
4339 'admin.',
4340 'webmaster.',
4341 'localhost.',
4342 'dev.',
4343 'development.',
4344 'test.',
4345 'stage.',
4346 'staging.',
4347 );
4348
4349 // Make sure domain is not one of the blacklisted.
4350 foreach ( $blacklist as $invalid ) {
4351 if ( 0 === strpos( $parts[1], $invalid ) ) {
4352 return false;
4353 }
4354 }
4355
4356 // Get the UTF encoded domain name.
4357 $domain = idn_to_ascii( $parts[1] ) . '.';
4358
4359 return ( checkdnsrr( $domain, 'MX' ) || checkdnsrr( $domain, 'A' ) );
4360 }
4361
4362 /**
4363 * Generate API connectivity issue message.
4364 *
4365 * @author Vova Feldman (@svovaf)
4366 * @since 1.0.9
4367 *
4368 * @param mixed $api_result
4369 * @param bool $is_first_failure
4370 */
4371 function _add_connectivity_issue_message( $api_result, $is_first_failure = true ) {
4372 if ( ! $this->is_premium() && $this->_enable_anonymous ) {
4373 // Don't add message if it's the free version and can run anonymously.
4374 return;
4375 }
4376
4377 if ( ! function_exists( 'wp_nonce_url' ) ) {
4378 require_once ABSPATH . 'wp-includes/functions.php';
4379 }
4380
4381 $current_user = self::_get_current_wp_user();
4382 // $admin_email = get_option( 'admin_email' );
4383 $admin_email = $current_user->user_email;
4384
4385 // Aliases.
4386 $deactivate_plugin_title = $this->esc_html_inline( 'That\'s exhausting, please deactivate', 'deactivate-plugin-title' );
4387 $deactivate_plugin_desc = $this->esc_html_inline( 'We feel your frustration and sincerely apologize for the inconvenience. Hope to see you again in the future.', 'deactivate-plugin-desc' );
4388 $install_previous_title = $this->esc_html_inline( 'Let\'s try your previous version', 'install-previous-title' );
4389 $install_previous_desc = $this->esc_html_inline( 'Uninstall this version and install the previous one.', 'install-previous-desc' );
4390 $fix_issue_title = $this->esc_html_inline( 'Yes - I\'m giving you a chance to fix it', 'fix-issue-title' );
4391 $fix_issue_desc = $this->esc_html_inline( 'We will do our best to whitelist your server and resolve this issue ASAP. You will get a follow-up email to %s once we have an update.', 'fix-issue-desc' );
4392 /* translators: %s: product title (e.g. "Awesome Plugin" requires access to...) */
4393 $x_requires_access_to_api = $this->esc_html_inline( '%s requires access to our API.', 'x-requires-access-to-api' );
4394 $sysadmin_title = $this->esc_html_inline( 'I\'m a system administrator', 'sysadmin-title' );
4395 $happy_to_resolve_issue_asap = $this->esc_html_inline( 'We are sure it\'s an issue on our side and more than happy to resolve it for you ASAP if you give us a chance.', 'happy-to-resolve-issue-asap' );
4396
4397 if ( $this->is_premium() ) {
4398 /* translators: This string is optionally prepended to 'plugin requires access to our API.' */
4399 $x_requires_access_to_api = $this->esc_html_inline( 'For automatic delivery of security & feature updates,', 'requires-api-for' ) . ' ' . $x_requires_access_to_api;
4400 }
4401
4402 $message = false;
4403 if ( is_object( $api_result ) &&
4404 isset( $api_result->error ) &&
4405 isset( $api_result->error->code )
4406 ) {
4407 switch ( $api_result->error->code ) {
4408 case 'curl_missing':
4409 $missing_methods = '';
4410 if ( is_array( $api_result->missing_methods ) &&
4411 ! empty( $api_result->missing_methods )
4412 ) {
4413 foreach ( $api_result->missing_methods as $m ) {
4414 if ( 'curl_version' === $m ) {
4415 continue;
4416 }
4417
4418 if ( ! empty( $missing_methods ) ) {
4419 $missing_methods .= ', ';
4420 }
4421
4422 $missing_methods .= sprintf( '<code>%s</code>', $m );
4423 }
4424
4425 if ( ! empty( $missing_methods ) ) {
4426 $missing_methods = sprintf(
4427 '<br><br><b>%s</b> %s',
4428 $this->esc_html_inline( 'Disabled method(s):', 'curl-disabled-methods' ),
4429 $missing_methods
4430 );
4431 }
4432 }
4433
4434 $message = sprintf(
4435 $x_requires_access_to_api . ' ' .
4436 $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' ) . ' ' .
4437 $missing_methods .
4438 ' %s',
4439 '<b>' . $this->get_plugin_name() . '</b>',
4440 sprintf(
4441 '<ol id="fs_firewall_issue_options"><li>%s</li><li>%s</li><li>%s</li></ol>',
4442 sprintf(
4443 '<a class="fs-resolve" data-type="curl" href="#"><b>%s</b></a>%s',
4444 $this->get_text_inline( 'I don\'t know what is cURL or how to install it, help me!', 'curl-missing-no-clue-title' ),
4445 ' - ' . sprintf(
4446 $this->get_text_inline( 'We\'ll make sure to contact your hosting company and resolve the issue. You will get a follow-up email to %s once we have an update.', 'curl-missing-no-clue-desc' ),
4447 '<a href="mailto:' . $admin_email . '">' . $admin_email . '</a>'
4448 )
4449 ),
4450 sprintf(
4451 '<b>%s</b> - %s',
4452 $sysadmin_title,
4453 esc_html( sprintf( $this->get_text_inline( 'Great, please install cURL and enable it in your php.ini file. In addition, search for the \'disable_functions\' directive in your php.ini file and remove any disabled methods starting with \'curl_\'. To make sure it was successfully activated, use \'phpinfo()\'. Once activated, deactivate the %s and reactivate it back again.', 'curl-missing-sysadmin-desc' ), $this->get_module_label( true ) ) )
4454 ),
4455 sprintf(
4456 '<a href="%s"><b>%s</b></a> - %s',
4457 wp_nonce_url( 'plugins.php?action=deactivate&amp;plugin=' . $this->_plugin_basename . '&amp;plugin_status=all&amp;paged=1&amp;s=', 'deactivate-plugin_' . $this->_plugin_basename ),
4458 $deactivate_plugin_title,
4459 $deactivate_plugin_desc
4460 )
4461 )
4462 );
4463 break;
4464 case 'cloudflare_ddos_protection':
4465 $message = sprintf(
4466 $x_requires_access_to_api . ' ' .
4467 $this->esc_html_inline( 'From unknown reason, CloudFlare, the firewall we use, blocks the connection.', 'cloudflare-blocks-connection-message' ) . ' ' .
4468 $happy_to_resolve_issue_asap .
4469 ' %s',
4470 '<b>' . $this->get_plugin_name() . '</b>',
4471 sprintf(
4472 '<ol id="fs_firewall_issue_options"><li>%s</li><li>%s</li><li>%s</li></ol>',
4473 sprintf(
4474 '<a class="fs-resolve" data-type="cloudflare" href="#"><b>%s</b></a>%s',
4475 $fix_issue_title,
4476 ' - ' . sprintf(
4477 $fix_issue_desc,
4478 '<a href="mailto:' . $admin_email . '">' . $admin_email . '</a>'
4479 )
4480 ),
4481 sprintf(
4482 '<a href="%s" target="_blank" rel="noopener noreferrer"><b>%s</b></a> - %s',
4483 sprintf( 'https://wordpress.org/plugins/%s/download/', $this->_slug ),
4484 $install_previous_title,
4485 $install_previous_desc
4486 ),
4487 sprintf(
4488 '<a href="%s"><b>%s</b></a> - %s',
4489 wp_nonce_url( 'plugins.php?action=deactivate&amp;plugin=' . $this->_plugin_basename . '&amp;plugin_status=all&amp;paged=1&amp;s=' . '', 'deactivate-plugin_' . $this->_plugin_basename ),
4490 $deactivate_plugin_title,
4491 $deactivate_plugin_desc
4492 )
4493 )
4494 );
4495 break;
4496 case 'squid_cache_block':
4497 $message = sprintf(
4498 $x_requires_access_to_api . ' ' .
4499 $this->esc_html_inline( 'It looks like your server is using Squid ACL (access control lists), which blocks the connection.', 'squid-blocks-connection-message' ) .
4500 ' %s',
4501 '<b>' . $this->get_plugin_name() . '</b>',
4502 sprintf(
4503 '<ol id="fs_firewall_issue_options"><li>%s</li><li>%s</li><li>%s</li></ol>',
4504 sprintf(
4505 '<a class="fs-resolve" data-type="squid" href="#"><b>%s</b></a> - %s',
4506 $this->esc_html_inline( 'I don\'t know what is Squid or ACL, help me!', 'squid-no-clue-title' ),
4507 sprintf(
4508 $this->esc_html_inline( 'We\'ll make sure to contact your hosting company and resolve the issue. You will get a follow-up email to %s once we have an update.', 'squid-no-clue-desc' ),
4509 '<a href="mailto:' . $admin_email . '">' . $admin_email . '</a>'
4510 )
4511 ),
4512 sprintf(
4513 '<b>%s</b> - %s',
4514 $sysadmin_title,
4515 sprintf(
4516 $this->esc_html_inline( 'Great, please whitelist the following domains: %s. Once you are done, deactivate the %s and activate it again.', 'squid-sysadmin-desc' ),
4517 // We use a filter since the plugin might require additional API connectivity.
4518 '<b>' . implode( ', ', $this->apply_filters( 'api_domains', array(
4519 'api.freemius.com',
4520 'wp.freemius.com'
4521 ) ) ) . '</b>',
4522 $this->_module_type
4523 )
4524 ),
4525 sprintf(
4526 '<a href="%s"><b>%s</b></a> - %s',
4527 wp_nonce_url( 'plugins.php?action=deactivate&amp;plugin=' . $this->_plugin_basename . '&amp;plugin_status=all&amp;paged=1&amp;s=', 'deactivate-plugin_' . $this->_plugin_basename ),
4528 $deactivate_plugin_title,
4529 $deactivate_plugin_desc
4530 )
4531 )
4532 );
4533 break;
4534 // default:
4535 // $message = $this->get_text_inline( 'connectivity-test-fails-message' );
4536 // break;
4537 }
4538 }
4539
4540 $message_id = 'failed_connect_api';
4541 $type = 'error';
4542
4543 $connectivity_test_fails_message = $this->esc_html_inline( 'From unknown reason, the API connectivity test failed.', 'connectivity-test-fails-message' );
4544
4545 if ( false === $message ) {
4546 if ( $is_first_failure ) {
4547 // First attempt failed.
4548 $message = sprintf(
4549 $x_requires_access_to_api . ' ' .
4550 $connectivity_test_fails_message . ' ' .
4551 $this->esc_html_inline( 'It\'s probably a temporary issue on our end. Just to be sure, with your permission, would it be o.k to run another connectivity test?', 'connectivity-test-maybe-temporary' ) . '<br><br>' .
4552 '%s',
4553 '<b>' . $this->get_plugin_name() . '</b>',
4554 sprintf(
4555 '<div id="fs_firewall_issue_options">%s %s</div>',
4556 sprintf(
4557 '<a class="button button-primary fs-resolve" data-type="retry_ping" href="#">%s</a>',
4558 $this->get_text_inline( 'Yes - do your thing', 'yes-do-your-thing' )
4559 ),
4560 sprintf(
4561 '<a href="%s" class="button">%s</a>',
4562 wp_nonce_url( 'plugins.php?action=deactivate&amp;plugin=' . $this->_plugin_basename . '&amp;plugin_status=all&amp;paged=1&amp;s=', 'deactivate-plugin_' . $this->_plugin_basename ),
4563 $this->get_text_inline( 'No - just deactivate', 'no-deactivate' )
4564 )
4565 )
4566 );
4567
4568 $message_id = 'failed_connect_api_first';
4569 $type = 'promotion';
4570 } else {
4571 // Second connectivity attempt failed.
4572 $message = sprintf(
4573 $x_requires_access_to_api . ' ' .
4574 $connectivity_test_fails_message . ' ' .
4575 $happy_to_resolve_issue_asap .
4576 ' %s',
4577 '<b>' . $this->get_plugin_name() . '</b>',
4578 sprintf(
4579 '<ol id="fs_firewall_issue_options"><li>%s</li><li>%s</li><li>%s</li></ol>',
4580 sprintf(
4581 '<a class="fs-resolve" data-type="general" href="#"><b>%s</b></a>%s',
4582 $fix_issue_title,
4583 ' - ' . sprintf(
4584 $fix_issue_desc,
4585 '<a href="mailto:' . $admin_email . '">' . $admin_email . '</a>'
4586 )
4587 ),
4588 sprintf(
4589 '<a href="%s" target="_blank" rel="noopener noreferrer"><b>%s</b></a> - %s',
4590 sprintf( 'https://wordpress.org/plugins/%s/download/', $this->_slug ),
4591 $install_previous_title,
4592 $install_previous_desc
4593 ),
4594 sprintf(
4595 '<a href="%s"><b>%s</b></a> - %s',
4596 wp_nonce_url( 'plugins.php?action=deactivate&amp;plugin=' . $this->_plugin_basename . '&amp;plugin_status=all&amp;paged=1&amp;s=', 'deactivate-plugin_' . $this->_plugin_basename ),
4597 $deactivate_plugin_title,
4598 $deactivate_plugin_desc
4599 )
4600 )
4601 );
4602 }
4603 }
4604
4605 $this->_admin_notices->add_sticky(
4606 $message,
4607 $message_id,
4608 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
4609 $type
4610 );
4611 }
4612
4613 /**
4614 * Handle user request to resolve connectivity issue.
4615 * This method will send an email to Freemius API technical staff for resolution.
4616 * The email will contain server's info and installed plugins (might be caching issue).
4617 *
4618 * @author Vova Feldman (@svovaf)
4619 * @since 1.0.9
4620 */
4621 function _email_about_firewall_issue() {
4622 check_admin_referer( 'fs_resolve_firewall_issues' );
4623
4624 if ( ! current_user_can( is_multisite() ? 'manage_options' : 'activate_plugins' ) ) {
4625 return;
4626 }
4627
4628 $this->_admin_notices->remove_sticky( 'failed_connect_api' );
4629
4630 $pong = $this->ping();
4631
4632 $is_connected = $this->get_api_plugin_scope()->is_valid_ping( $pong );
4633
4634 if ( $is_connected ) {
4635 FS_GDPR_Manager::instance()->store_is_required( $pong->is_gdpr_required );
4636
4637 $this->store_connectivity_info( $pong, $is_connected );
4638
4639 echo $this->get_after_plugin_activation_redirect_url();
4640 exit;
4641 }
4642
4643 $current_user = self::_get_current_wp_user();
4644 $admin_email = $current_user->user_email;
4645
4646 $error_type = fs_request_get( 'error_type', 'general' );
4647
4648 switch ( $error_type ) {
4649 case 'squid':
4650 $title = 'Squid ACL Blocking Issue';
4651 break;
4652 case 'cloudflare':
4653 $title = 'CloudFlare Blocking Issue';
4654 break;
4655 default:
4656 $title = 'API Connectivity Issue';
4657 break;
4658 }
4659
4660 $custom_email_sections = array();
4661
4662 // Add 'API Error' custom email section.
4663 $custom_email_sections['api_error'] = array(
4664 'title' => 'API Error',
4665 'rows' => array(
4666 'ping' => array(
4667 'API Error',
4668 is_string( $pong ) ? htmlentities( $pong ) : json_encode( $pong )
4669 ),
4670 )
4671 );
4672
4673 // Send email with technical details to resolve API connectivity issues.
4674 $this->send_email(
4675 'api@freemius.com', // recipient
4676 $title . ' [' . $this->get_plugin_name() . ']', // subject
4677 $custom_email_sections,
4678 array( "Reply-To: $admin_email <$admin_email>" ) // headers
4679 );
4680
4681 $this->_admin_notices->add_sticky(
4682 sprintf(
4683 $this->get_text_inline( 'Thank for giving us the chance to fix it! A message was just sent to our technical staff. We will get back to you as soon as we have an update to %s. Appreciate your patience.', 'fix-request-sent-message' ),
4684 '<a href="mailto:' . $admin_email . '">' . $admin_email . '</a>'
4685 ),
4686 'server_details_sent'
4687 );
4688
4689 // Action was taken, tell that API connectivity troubleshooting should be off now.
4690
4691 echo "1";
4692 exit;
4693 }
4694
4695 /**
4696 * Handle connectivity test retry approved by the user.
4697 *
4698 * @author Vova Feldman (@svovaf)
4699 * @since 1.1.7.4
4700 */
4701 function _retry_connectivity_test() {
4702 check_admin_referer( 'fs_retry_connectivity_test' );
4703
4704 if ( ! current_user_can( is_multisite() ? 'manage_options' : 'activate_plugins' ) ) {
4705 return;
4706 }
4707
4708 $this->_admin_notices->remove_sticky( 'failed_connect_api_first' );
4709
4710 $pong = $this->ping();
4711
4712 $is_connected = $this->get_api_plugin_scope()->is_valid_ping( $pong );
4713
4714 if ( $is_connected ) {
4715 FS_GDPR_Manager::instance()->store_is_required( $pong->is_gdpr_required );
4716
4717 $this->store_connectivity_info( $pong, $is_connected );
4718
4719 echo $this->get_after_plugin_activation_redirect_url();
4720 } else {
4721 // Add connectivity issue message after 2nd failed attempt.
4722 $this->_add_connectivity_issue_message( $pong, false );
4723
4724 echo "1";
4725 }
4726
4727 exit;
4728 }
4729
4730 static function _add_firewall_issues_javascript() {
4731 $params = array();
4732 fs_require_once_template( 'firewall-issues-js.php', $params );
4733 }
4734
4735 #endregion
4736
4737 #----------------------------------------------------------------------------------
4738 #region Email
4739 #----------------------------------------------------------------------------------
4740
4741 /**
4742 * Generates and sends an HTML email with customizable sections.
4743 *
4744 * @author Leo Fajardo (@leorw)
4745 * @since 1.1.2
4746 *
4747 * @param string $to_address
4748 * @param string $subject
4749 * @param array $sections
4750 * @param array $headers
4751 *
4752 * @return bool Whether the email contents were sent successfully.
4753 */
4754 private function send_email(
4755 $to_address,
4756 $subject,
4757 $sections = array(),
4758 $headers = array()
4759 ) {
4760 $default_sections = $this->get_email_sections();
4761
4762 // Insert new sections or replace the default email sections.
4763 if ( is_array( $sections ) && ! empty( $sections ) ) {
4764 foreach ( $sections as $section_id => $custom_section ) {
4765 if ( ! isset( $default_sections[ $section_id ] ) ) {
4766 // If the section does not exist, add it.
4767 $default_sections[ $section_id ] = $custom_section;
4768 } else {
4769 // If the section already exists, override it.
4770 $current_section = $default_sections[ $section_id ];
4771
4772 // Replace the current section's title if a custom section title exists.
4773 if ( isset( $custom_section['title'] ) ) {
4774 $current_section['title'] = $custom_section['title'];
4775 }
4776
4777 // Insert new rows under the current section or replace the default rows.
4778 if ( isset( $custom_section['rows'] ) && is_array( $custom_section['rows'] ) && ! empty( $custom_section['rows'] ) ) {
4779 foreach ( $custom_section['rows'] as $row_id => $row ) {
4780 $current_section['rows'][ $row_id ] = $row;
4781 }
4782 }
4783
4784 $default_sections[ $section_id ] = $current_section;
4785 }
4786 }
4787 }
4788
4789 $vars = array( 'sections' => $default_sections );
4790 $message = fs_get_template( 'email.php', $vars );
4791
4792 // Set the type of email to HTML.
4793 $headers[] = 'Content-type: text/html; charset=UTF-8';
4794
4795 $header_string = implode( "\r\n", $headers );
4796
4797 return wp_mail(
4798 $to_address,
4799 $subject,
4800 $message,
4801 $header_string
4802 );
4803 }
4804
4805 /**
4806 * Generates the data for the sections of the email content.
4807 *
4808 * @author Leo Fajardo (@leorw)
4809 * @since 1.1.2
4810 *
4811 * @return array
4812 */
4813 private function get_email_sections() {
4814 // Retrieve the current user's information so that we can get the user's email, first name, and last name below.
4815 $current_user = self::_get_current_wp_user();
4816
4817 // Retrieve the cURL version information so that we can get the version number below.
4818 $curl_version_information = curl_version();
4819
4820 $active_plugin = self::get_active_plugins();
4821
4822 // Generate the list of active plugins separated by new line.
4823 $active_plugin_string = '';
4824 foreach ( $active_plugin as $plugin ) {
4825 $active_plugin_string .= sprintf(
4826 '<a href="%s">%s</a> [v%s]<br>',
4827 $plugin['PluginURI'],
4828 $plugin['Name'],
4829 $plugin['Version']
4830 );
4831 }
4832
4833 $server_ip = WP_FS__REMOTE_ADDR;
4834
4835 // Add PHP info for deeper investigation.
4836 ob_start();
4837 phpinfo();
4838 $php_info = ob_get_clean();
4839
4840 $api_domain = substr( FS_API__ADDRESS, strpos( FS_API__ADDRESS, ':' ) + 3 );
4841
4842 // Generate the default email sections.
4843 $sections = array(
4844 'sdk' => array(
4845 'title' => 'SDK',
4846 'rows' => array(
4847 'fs_version' => array( 'FS Version', $this->version ),
4848 'curl_version' => array( 'cURL Version', $curl_version_information['version'] )
4849 )
4850 ),
4851 'plugin' => array(
4852 'title' => ucfirst( $this->get_module_type() ),
4853 'rows' => array(
4854 'name' => array( 'Name', $this->get_plugin_name() ),
4855 'version' => array( 'Version', $this->get_plugin_version() )
4856 )
4857 ),
4858 'api' => array(
4859 'title' => 'API Subdomain',
4860 'rows' => array(
4861 'dns' => array(
4862 'DNS_CNAME',
4863 function_exists( 'dns_get_record' ) ?
4864 var_export( dns_get_record( $api_domain, DNS_CNAME ), true ) :
4865 'dns_get_record() disabled/blocked'
4866 ),
4867 'ip' => array(
4868 'IP',
4869 function_exists( 'gethostbyname' ) ?
4870 gethostbyname( $api_domain ) :
4871 'gethostbyname() disabled/blocked'
4872 ),
4873 ),
4874 ),
4875 'site' => array(
4876 'title' => 'Site',
4877 'rows' => array(
4878 'unique_id' => array( 'Unique ID', $this->get_anonymous_id() ),
4879 'address' => array( 'Address', site_url() ),
4880 'host' => array(
4881 'HTTP_HOST',
4882 ( ! empty( $_SERVER['HTTP_HOST'] ) ? $_SERVER['HTTP_HOST'] : '' )
4883 ),
4884 'hosting' => array(
4885 'Hosting Company' => fs_request_has( 'hosting_company' ) ?
4886 fs_request_get( 'hosting_company' ) :
4887 'Unknown',
4888 ),
4889 'server_addr' => array(
4890 'SERVER_ADDR',
4891 '<a href="http://www.projecthoneypot.org/ip_' . $server_ip . '">' . $server_ip . '</a>'
4892 )
4893 )
4894 ),
4895 'user' => array(
4896 'title' => 'User',
4897 'rows' => array(
4898 'email' => array( 'Email', $current_user->user_email ),
4899 'first' => array( 'First', $current_user->user_firstname ),
4900 'last' => array( 'Last', $current_user->user_lastname )
4901 )
4902 ),
4903 'plugins' => array(
4904 'title' => 'Plugins',
4905 'rows' => array(
4906 'active_plugins' => array( 'Active Plugins', $active_plugin_string )
4907 )
4908 ),
4909 'php_info' => array(
4910 'title' => 'PHP Info',
4911 'rows' => array(
4912 'info' => array( $php_info )
4913 ),
4914 )
4915 );
4916
4917 // Allow the sections to be modified by other code.
4918 $sections = $this->apply_filters( 'email_template_sections', $sections );
4919
4920 return $sections;
4921 }
4922
4923 #endregion
4924
4925 #----------------------------------------------------------------------------------
4926 #region Initialization
4927 #----------------------------------------------------------------------------------
4928
4929 /**
4930 * Init plugin's Freemius instance.
4931 *
4932 * @author Vova Feldman (@svovaf)
4933 * @since 1.0.1
4934 *
4935 * @param number $id
4936 * @param string $public_key
4937 * @param bool $is_live
4938 * @param bool $is_premium
4939 */
4940 function init( $id, $public_key, $is_live = true, $is_premium = true ) {
4941 $this->_logger->entrance();
4942
4943 $this->dynamic_init( array(
4944 'id' => $id,
4945 'public_key' => $public_key,
4946 'is_live' => $is_live,
4947 'is_premium' => $is_premium,
4948 ) );
4949 }
4950
4951 /**
4952 * Dynamic initiator, originally created to support initiation
4953 * with parent_id for add-ons.
4954 *
4955 * @author Vova Feldman (@svovaf)
4956 * @since 1.0.6
4957 *
4958 * @param array $plugin_info
4959 *
4960 * @throws Freemius_Exception
4961 */
4962 function dynamic_init( array $plugin_info ) {
4963 $this->_logger->entrance();
4964
4965 $this->parse_settings( $plugin_info );
4966
4967 $this->register_after_settings_parse_hooks();
4968
4969 if ( $this->should_stop_execution() ) {
4970 return;
4971 }
4972
4973 if ( ! $this->is_registered() ) {
4974 if ( $this->is_anonymous() ) {
4975 // If user skipped, no need to test connectivity.
4976 $this->_has_api_connection = true;
4977 $this->_is_on = true;
4978 } else {
4979 if ( ! $this->has_api_connectivity() ) {
4980 if ( $this->_admin_notices->has_sticky( 'failed_connect_api_first' ) ||
4981 $this->_admin_notices->has_sticky( 'failed_connect_api' )
4982 ) {
4983 if ( ! $this->_enable_anonymous || $this->is_premium() ) {
4984 // If anonymous mode is disabled, add firewall admin-notice message.
4985 add_action( 'admin_footer', array( 'Freemius', '_add_firewall_issues_javascript' ) );
4986
4987 $ajax_action_suffix = $this->_slug . ( $this->is_theme() ? ':theme' : '' );
4988 add_action( "wp_ajax_fs_resolve_firewall_issues_{$ajax_action_suffix}", array(
4989 &$this,
4990 '_email_about_firewall_issue'
4991 ) );
4992
4993 add_action( "wp_ajax_fs_retry_connectivity_test_{$ajax_action_suffix}", array(
4994 &$this,
4995 '_retry_connectivity_test'
4996 ) );
4997
4998 /**
4999 * Currently the admin notice manager relies on the module's type and slug. The new AJAX actions manager uses module IDs, hence, consider to replace the if block above with the commented code below after adjusting the admin notices manager to work with module IDs.
5000 *
5001 * @author Vova Feldman (@svovaf)
5002 * @since 2.0.0
5003 */
5004 /*$this->add_ajax_action( 'resolve_firewall_issues', array(
5005 &$this,
5006 '_email_about_firewall_issue'
5007 ) );
5008
5009 $this->add_ajax_action( 'retry_connectivity_test', array(
5010 &$this,
5011 '_retry_connectivity_test'
5012 ) );*/
5013 }
5014 }
5015
5016 return;
5017 } else {
5018 $this->_admin_notices->remove_sticky( array(
5019 'failed_connect_api_first',
5020 'failed_connect_api',
5021 ) );
5022
5023 if ( $this->_anonymous_mode ) {
5024 // Simulate anonymous mode.
5025 $this->_is_anonymous = true;
5026 }
5027 }
5028 }
5029 }
5030
5031 /**
5032 * This should be executed even if Freemius is off for the core module,
5033 * otherwise, the add-ons dialog box won't work properly. This is especially
5034 * relevant when the developer decided to turn FS off for existing users.
5035 *
5036 * @author Vova Feldman (@svovaf)
5037 */
5038 if ( $this->is_user_in_admin() &&
5039 'plugin-information' === fs_request_get( 'tab', false ) &&
5040 $this->should_use_freemius_updater_and_dialog() &&
5041 (
5042 ( $this->is_addon() && $this->get_slug() == fs_request_get( 'plugin', false ) ) ||
5043 ( $this->has_addons() && $this->get_id() == fs_request_get( 'parent_plugin_id', false ) )
5044 )
5045 ) {
5046 require_once WP_FS__DIR_INCLUDES . '/fs-plugin-info-dialog.php';
5047
5048 new FS_Plugin_Info_Dialog( $this->is_addon() ? $this->get_parent_instance() : $this );
5049 }
5050
5051 // Check if Freemius is on for the current plugin.
5052 // This MUST be executed after all the plugin variables has been loaded.
5053 if ( ! $this->is_registered() && ! $this->is_on() ) {
5054 return;
5055 }
5056
5057 if ( $this->has_api_connectivity() ) {
5058 if ( self::is_cron() ) {
5059 $this->hook_callback_to_sync_cron();
5060 } else if ( $this->is_user_in_admin() ) {
5061 /**
5062 * Schedule daily data sync cron if:
5063 *
5064 * 1. User opted-in (for tracking).
5065 * 2. If skipped, but later upgraded (opted-in via upgrade).
5066 *
5067 * @author Vova Feldman (@svovaf)
5068 * @since 1.1.7.3
5069 *
5070 */
5071 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
5072 $this->maybe_schedule_sync_cron();
5073 }
5074
5075 /**
5076 * Check if requested for manual blocking background sync.
5077 */
5078 if ( fs_request_has( 'background_sync' ) ) {
5079 self::require_pluggable_essentials();
5080 self::wp_cookie_constants();
5081
5082 $this->run_manual_sync();
5083 }
5084 }
5085 }
5086
5087 if ( $this->is_registered() ) {
5088 FS_Clone_Manager::instance()->maybe_resolve_new_subsite_install_automatically( $this );
5089
5090 $this->hook_callback_to_install_sync();
5091 }
5092
5093 if ( $this->is_addon() ) {
5094 if ( $this->is_parent_plugin_installed() ) {
5095 // Link to parent FS.
5096 $this->_parent = self::get_instance_by_id( $this->_plugin->parent_plugin_id );
5097
5098 // Get parent plugin reference.
5099 $this->_parent_plugin = $this->_parent->get_plugin();
5100 }
5101 }
5102
5103 if ( $this->is_user_in_admin() ) {
5104 if ( $this->is_registered() && fs_request_has( 'purchase_completed' ) ) {
5105 $this->_admin_notices->add_sticky(
5106 sprintf(
5107 /* translators: %s: License type (e.g. you have a professional license) */
5108 $this->get_text_inline( 'You have purchased a %s license.', 'you-have-x-license' ),
5109 fs_request_get( 'purchased_plan' )
5110 ) .
5111 sprintf(
5112 $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' ),
5113 $this->get_module_label( true ),
5114 ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? "products' " : '' ),
5115 ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? 's' : '' ),
5116 sprintf(
5117 '<strong>%s</strong>',
5118 fs_request_get( 'purchase_email' )
5119 )
5120 ),
5121 'plan_purchased',
5122 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
5123 );
5124 }
5125
5126 if ( $this->is_addon() ) {
5127 if ( ! $this->is_parent_plugin_installed() ) {
5128 $parent_name = $this->get_option( $plugin_info, 'parent_name', null );
5129
5130 if ( isset( $plugin_info['parent'] ) ) {
5131 $parent_name = $this->get_option( $plugin_info['parent'], 'name', null );
5132 }
5133
5134 $this->_admin_notices->add(
5135 ( ! empty( $parent_name ) ?
5136 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 ) :
5137 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() )
5138 ),
5139 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
5140 'error'
5141 );
5142
5143 return;
5144 } else {
5145 $is_network_admin = fs_is_network_admin();
5146
5147 if ( ! $this->_parent->is_registered() && $this->is_registered() ) {
5148 // If add-on activated and parent not, automatically install parent for the user.
5149 $this->activate_parent_account( $this->_parent );
5150 } else if (
5151 $this->_parent->is_registered() &&
5152 ! $this->is_registered() &&
5153 /**
5154 * If not registered for add-on and the following conditions for the add-on are met, activate add-on account.
5155 * * Network active and in network admin - network activate add-on account.
5156 * * Network active and not in network admin - activate add-on account for the current blog.
5157 * * Not network active and not in network admin - activate add-on account for the current blog.
5158 *
5159 * If not registered for add-on, not network active, and in network admin, do not handle the add-on activation.
5160 *
5161 * @author Leo Fajardo (@leorw)
5162 * @since 2.3.0
5163 */
5164 ( $this->is_network_active() || ! $is_network_admin )
5165 ) {
5166 $premium_license = null;
5167
5168 if (
5169 ! $this->has_free_plan() &&
5170 $this->is_bundle_license_auto_activation_enabled() &&
5171 $this->_parent->is_activated_with_bundle_license()
5172 ) {
5173 /**
5174 * If the add-on has no free plan, try to activate the account only when there's a bundle license.
5175 *
5176 * @author Leo Fajardo (@leorw)
5177 * @since 2.4.0
5178 */
5179 $bundle_license = $this->get_active_parent_license( $this->_parent->_get_license()->secret_key, false );
5180
5181 if (
5182 is_object( $bundle_license ) &&
5183 ! empty( $bundle_license->products ) &&
5184 in_array( $this->get_id(), $bundle_license->products )
5185 ) {
5186 $premium_license = $bundle_license;
5187 }
5188 }
5189
5190 if ( $this->has_free_plan() || is_object( $premium_license) ) {
5191 // If parent plugin activated, automatically install add-on for the user.
5192 $this->_activate_addon_account(
5193 $this->_parent,
5194 ( $this->is_network_active() && $is_network_admin ) ?
5195 true :
5196 get_current_blog_id(),
5197 $premium_license
5198 );
5199 }
5200 }
5201
5202 // @todo This should be only executed on activation. It should be migrated to register_activation_hook() together with other activation related logic.
5203 if ( $this->is_premium() ) {
5204 // Remove add-on download admin-notice.
5205 $this->_parent->_admin_notices->remove_sticky( array(
5206 'addon_plan_upgraded_' . $this->_slug,
5207 'no_addon_license_' . $this->_slug,
5208 ) );
5209 }
5210
5211 // $this->deactivate_premium_only_addon_without_license();
5212 }
5213 }
5214
5215 add_action( 'admin_init', array( &$this, '_admin_init_action' ) );
5216
5217 // if ( $this->is_registered() ||
5218 // $this->is_anonymous() ||
5219 // $this->is_pending_activation()
5220 // ) {
5221 // $this->_init_admin();
5222 // }
5223 }
5224
5225 /**
5226 * Should be called outside `$this->is_user_in_admin()` scope
5227 * because the updater has some logic that needs to be executed
5228 * during AJAX calls.
5229 *
5230 * Currently, we need to hook to the `http_request_host_is_external` filter.
5231 * In the future, there might be additional logic added.
5232 *
5233 * @author Vova Feldman
5234 * @since 1.2.1.6
5235 */
5236 if (
5237 $this->should_use_freemius_updater_and_dialog() &&
5238 (
5239 $this->is_premium() ||
5240 /**
5241 * If not premium but the premium version is installed, also instantiate the updater so that the
5242 * plugin information dialog of the premium version will have the information from the server.
5243 *
5244 * @author Leo Fajardo (@leorw)
5245 * @since 2.2.3
5246 */
5247 ( file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $this->premium_plugin_basename() ) ) )
5248 ) &&
5249 $this->has_release_on_freemius() &&
5250 ( ! $this->is_unresolved_clone( true ) )
5251 ) {
5252 FS_Plugin_Updater::instance( $this );
5253 }
5254
5255 $this->do_action( 'initiated' );
5256
5257 if ( $this->_storage->prev_is_premium !== $this->_plugin->is_premium ) {
5258 if ( isset( $this->_storage->prev_is_premium ) ) {
5259 $this->apply_filters(
5260 'after_code_type_change',
5261 // New code type.
5262 $this->_plugin->is_premium
5263 );
5264 } else {
5265 // Set for code type for the first time.
5266 $this->_storage->prev_is_premium = $this->_plugin->is_premium;
5267 }
5268 }
5269
5270 if ( ! $this->is_addon() ) {
5271 if ( $this->is_registered() ) {
5272 // Fix for upgrade from versions < 1.0.9.
5273 if ( ! isset( $this->_storage->activation_timestamp ) ) {
5274 $this->_storage->activation_timestamp = WP_FS__SCRIPT_START_TIME;
5275 }
5276
5277 $this->do_action( 'after_init_plugin_registered' );
5278 } else if ( $this->is_anonymous() ) {
5279 $this->do_action( 'after_init_plugin_anonymous' );
5280 } else if ( $this->is_pending_activation() ) {
5281 $this->do_action( 'after_init_plugin_pending_activations' );
5282 }
5283 } else {
5284 if ( $this->is_registered() ) {
5285 $this->do_action( 'after_init_addon_registered' );
5286 } else if ( $this->is_anonymous() ) {
5287 $this->do_action( 'after_init_addon_anonymous' );
5288 } else if ( $this->is_pending_activation() ) {
5289 $this->do_action( 'after_init_addon_pending_activations' );
5290 }
5291 }
5292 }
5293
5294 /**
5295 * @author Leo Fajardo (@leorw)
5296 * @since 2.2.3
5297 *
5298 * @return bool
5299 */
5300 private function should_use_freemius_updater_and_dialog() {
5301 return (
5302 /**
5303 * Allow updater and dialog when the `fs_allow_updater_and_dialog` URL query param exists and has `true`
5304 * value, or when the current page is not the "Add Plugins" page (/plugin-install.php) and the `action`
5305 * URL query param doesn't exist or its value is not `install-plugin` so that there will be no conflicts
5306 * with the .org plugins' functionalities (e.g. installation from the "Add Plugins" page and viewing
5307 * plugin details from .org).
5308 */
5309 ( true === fs_request_get_bool( 'fs_allow_updater_and_dialog' ) ) ||
5310 (
5311 ! self::is_plugin_install_page() &&
5312 // Disallow updater and dialog when installing a plugin, otherwise .org "add-on" plugins will be affected.
5313 ( 'install-plugin' !== fs_request_get( 'action' ) )
5314 )
5315 );
5316 }
5317
5318 /**
5319 * @param string[] $permissions
5320 * @param bool $is_enabled
5321 * @param int|null $blog_id
5322 *
5323 * @return true|object `true` on success, API error object on failure.
5324 */
5325 private function update_site_permissions( array $permissions, $is_enabled, $blog_id = null ) {
5326 $this->_logger->entrance();
5327
5328 $params = array(
5329 'permissions' => implode( ',', $permissions ),
5330 'is_enabled' => $is_enabled,
5331 );
5332
5333 $current_blog_id = get_current_blog_id();
5334 $is_blog_switched = false;
5335 if ( is_numeric( $blog_id ) && $current_blog_id != $blog_id ) {
5336 $is_blog_switched = $this->switch_to_blog( $blog_id );
5337 }
5338
5339 $result = $this->api_site_call( '/permissions.json', 'put', $params );
5340
5341 if ( $is_blog_switched ) {
5342 $this->switch_to_blog( $current_blog_id );
5343 }
5344
5345 if (
5346 ! $this->is_api_result_object( $result ) ||
5347 ! isset( $result->install_id )
5348 ) {
5349 $this->_logger->api_error( $result );
5350
5351 return $result;
5352 }
5353
5354 return true;
5355 }
5356
5357 /**
5358 * @param string[] $permissions
5359 * @param bool $is_enabled
5360 * @param bool $has_site_delegated_connection
5361 *
5362 * @return true|object `true` on success, API error object on failure.
5363 */
5364 private function update_network_permissions(
5365 array $permissions,
5366 $is_enabled,
5367 &$has_site_delegated_connection
5368 ) {
5369 $this->_logger->entrance();
5370
5371 $install_id_2_blog_id = array();
5372 $install_by_blog_id = $this->get_blog_install_map();
5373
5374 $has_site_delegated_connection = false;
5375
5376 foreach ( $install_by_blog_id as $blog_id => $install ) {
5377 if ( $this->is_site_delegated_connection( $blog_id ) ) {
5378 // Only update permissions of non-delegated installs.
5379 $has_site_delegated_connection = true;
5380 continue;
5381 }
5382
5383 $install_id_2_blog_id[ $install->id ] = $blog_id;
5384 }
5385
5386 if ( empty( $install_id_2_blog_id ) ) {
5387 return true;
5388 }
5389
5390 $params = array(
5391 'permissions' => implode( ',', $permissions ),
5392 'is_enabled' => $is_enabled,
5393 'install_ids' => implode( ',', array_keys( $install_id_2_blog_id ) ),
5394 );
5395
5396 // Send update to FS.
5397 $result = $this->get_current_or_network_user_api_scope()->call(
5398 "/plugins/{$this->_module_id}/installs/permissions.json",
5399 'put',
5400 $params
5401 );
5402
5403 if ( ! $this->is_api_result_object( $result, 'installs_metadata' ) ) {
5404 $this->_logger->api_error( $result );
5405
5406 return $result;
5407 }
5408
5409 return true;
5410 }
5411
5412 /**
5413 * @param mixed $result
5414 *
5415 * @return string
5416 */
5417 private function get_api_error_message( $result ) {
5418 $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:',
5419 'unexpected-api-error' ), $this->_module_type ) . ' ';
5420
5421 if (
5422 $this->is_api_error( $result ) &&
5423 isset( $result->error )
5424 ) {
5425 $code = empty( $result->error->code ) ? '' : " Code: {$result->error->code}";
5426
5427 $error_message .= "<b>{$result->error->message}{$code}</b>";
5428 } else {
5429 $error_message .= var_export( $result, true );
5430 }
5431
5432 return $error_message;
5433 }
5434
5435 /**
5436 * @author Vova Feldman (@svovaf)
5437 * @since 2.5.1
5438 */
5439 function _toggle_permission_tracking_callback() {
5440 $this->_logger->entrance();
5441
5442 $this->check_ajax_referer( 'toggle_permission_tracking' );
5443
5444 if ( ! $this->is_registered( true ) ) {
5445 self::shoot_ajax_failure( 'User never opted-in.' );
5446 }
5447
5448 $is_enabled = fs_request_get_bool( 'is_enabled' );
5449 $permissions = fs_request_get( 'permissions' );
5450
5451 if ( ! is_string( $permissions ) ) {
5452 self::shoot_ajax_failure( 'The permissions param must be a string.' );
5453 }
5454
5455 $permissions = explode( ',', $permissions );
5456
5457 $result = $this->toggle_permission_tracking( $permissions, $is_enabled );
5458
5459 if ( true !== $result ) {
5460 self::shoot_ajax_failure( $this->get_api_error_message( $result ) );
5461 }
5462
5463 self::shoot_ajax_success();
5464 }
5465
5466 /**
5467 * @param string[] $permissions
5468 * @param bool $is_enabled
5469 * @param int|null $blog_id
5470 *
5471 * @return bool|mixed `true` if updated successfully or no update is needed.
5472 */
5473 private function toggle_permission_tracking( $permissions, $is_enabled, $blog_id = null ) {
5474 if ( ! $this->is_registered( true ) ) {
5475 // User never opted-in.
5476 return false;
5477 }
5478
5479 // Check if permissions are already set as needed.
5480 if ( FS_Permission_Manager::instance( $this )->are_permissions( $permissions, $is_enabled, $blog_id ) ) {
5481 /**
5482 * Note:
5483 * 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.
5484 */
5485 return true;
5486 }
5487
5488 $api_managed_permissions = array_intersect(
5489 $permissions,
5490 FS_Permission_Manager::get_api_managed_permission_ids()
5491 );
5492
5493 if (
5494 in_array( FS_Permission_Manager::PERMISSION_ESSENTIALS, $permissions ) &&
5495 ! in_array( FS_Permission_Manager::PERMISSION_SITE, $permissions )
5496 ) {
5497 $api_managed_permissions[] = FS_Permission_Manager::PERMISSION_SITE;
5498 }
5499
5500 if ( ! empty( $api_managed_permissions ) ) {
5501 $has_site_delegated_connection = false;
5502
5503 if (
5504 ! $is_enabled &&
5505 ! in_array( FS_Permission_Manager::PERMISSION_EXTENSIONS, $api_managed_permissions ) &&
5506 false === FS_Permission_Manager::instance( $this )->is_extensions_tracking_allowed( $blog_id )
5507 ) {
5508 /**
5509 * 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.
5510 *
5511 * @todo Remove this entire `if` after implementing granular opt-in that also sends the permissions to the API when opting in.
5512 */
5513 $api_managed_permissions[] = FS_Permission_Manager::PERMISSION_EXTENSIONS;
5514 }
5515
5516 if ( is_null( $blog_id ) && fs_is_network_admin() ) {
5517 $result = $this->update_network_permissions(
5518 $api_managed_permissions,
5519 $is_enabled,
5520 $has_site_delegated_connection
5521 );
5522 } else {
5523 $result = $this->update_site_permissions(
5524 $api_managed_permissions,
5525 $is_enabled,
5526 $blog_id
5527 );
5528 }
5529
5530 if ( true !== $result ) {
5531 return $result;
5532 }
5533
5534 if ( in_array( FS_Permission_Manager::PERMISSION_SITE, $api_managed_permissions ) ) {
5535 if ( $is_enabled ) {
5536 $this->schedule_sync_cron();
5537 } else {
5538 $this->clear_sync_cron( ! $has_site_delegated_connection );
5539 }
5540 }
5541
5542 if ( in_array( FS_Permission_Manager::PERMISSION_USER, $api_managed_permissions ) ) {
5543 $this->toggle_user_permission( $is_enabled, $blog_id );
5544 }
5545 }
5546
5547 $this->update_tracking_permissions(
5548 $permissions,
5549 $is_enabled,
5550 $blog_id
5551 );
5552
5553 return true;
5554 }
5555
5556 /**
5557 * @param bool $is_enabled
5558 * @param int|null $blog_id
5559 */
5560 private function toggle_user_permission( $is_enabled, $blog_id = null ) {
5561 $network_or_blog_ids = is_numeric( $blog_id ) ?
5562 $blog_id :
5563 fs_is_network_admin();
5564
5565 if ( $is_enabled ) {
5566 $this->reset_anonymous_mode( $network_or_blog_ids );
5567 } else {
5568 $this->skip_connection( $network_or_blog_ids );
5569 }
5570 }
5571
5572 /**
5573 * Opt-in back into usage tracking.
5574 *
5575 * Note: This will only work if the user opted-in previously.
5576 *
5577 * Returns:
5578 * 1. FALSE - If the user never opted-in.
5579 * 2. TRUE - If successfully opted-in back to usage tracking.
5580 * 3. object - API result on failure.
5581 *
5582 * @author Leo Fajardo (@leorw)
5583 * @since 1.2.1.5
5584 *
5585 * @bool $is_enabled
5586 *
5587 * @return bool|object
5588 */
5589 private function toggle_site_tracking( $is_enabled, $blog_id = null ) {
5590 $this->_logger->entrance();
5591
5592 return $this->toggle_permission_tracking(
5593 FS_Permission_Manager::instance( $this )->get_site_tracking_permission_names(),
5594 $is_enabled,
5595 $blog_id
5596 );
5597 }
5598
5599 /**
5600 * If user opted-in and later disabled usage-tracking,
5601 * re-allow tracking for licensing and updates.
5602 *
5603 * @author Leo Fajardo (@leorw)
5604 * @since 1.2.1.5
5605 *
5606 * @param bool $is_context_single_site
5607 */
5608 private function reconnect_locally( $is_context_single_site = false ) {
5609 $this->_logger->entrance();
5610
5611 if ( ! $this->is_registered() ) {
5612 return;
5613 }
5614
5615 if ( ! fs_is_network_admin() || $is_context_single_site ) {
5616 if ( $this->is_tracking_prohibited() ) {
5617 FS_Permission_Manager::instance( $this )->update_site_tracking( true );
5618 }
5619 } else {
5620 $installs_map = $this->get_blog_install_map();
5621 foreach ( $installs_map as $blog_id => $install ) {
5622 /**
5623 * @var FS_Site $install
5624 */
5625 if ( ! $this->is_tracking_allowed( $blog_id, $install ) ) {
5626 FS_Permission_Manager::instance( $this )->update_site_tracking( true, $blog_id );
5627 }
5628 }
5629 }
5630 }
5631
5632 /**
5633 * 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.
5634 *
5635 * @param string[] $permissions
5636 * @param bool $is_enabled
5637 * @param int|null $blog_id
5638 *
5639 * @return array
5640 */
5641 private function update_tracking_permissions( $permissions, $is_enabled, $blog_id = null ) {
5642 // Alias.
5643 $permission_manager = FS_Permission_Manager::instance( $this );
5644
5645 $network_or_blog_ids = is_numeric( $blog_id ) ?
5646 $blog_id :
5647 fs_is_network_admin();
5648
5649 if ( true === $network_or_blog_ids ) {
5650 // Update the permission for all non-delegated sub-sites.
5651 $blog_ids = $this->get_non_delegated_blog_ids();
5652
5653 // Add the network-level to the array, to update the permission on the network-level storage.
5654 array_unshift( $blog_ids, null );
5655 }
5656 else
5657 {
5658 if ( false === $network_or_blog_ids ) {
5659 $network_or_blog_ids = null;
5660 }
5661
5662 $blog_ids = is_array( $network_or_blog_ids ) ?
5663 $network_or_blog_ids :
5664 array( $network_or_blog_ids );
5665 }
5666
5667 $result = array();
5668 foreach ( $permissions as $permission ) {
5669 $permission = trim( $permission );
5670 $is_permission_supported = true;
5671
5672 foreach ( $blog_ids as $id ) {
5673 $is_permission_supported = $permission_manager->update_permission_tracking_flag(
5674 $permission,
5675 $is_enabled,
5676 $id
5677 );
5678 }
5679
5680 if ( ! $is_permission_supported ) {
5681 $permission = 'no_match';
5682 }
5683
5684 $result[ $permission ] = $is_enabled;
5685 }
5686
5687 return $result;
5688 }
5689
5690 /**
5691 * Parse plugin's settings (as defined by the plugin dev).
5692 *
5693 * @author Vova Feldman (@svovaf)
5694 * @since 1.1.7.3
5695 *
5696 * @param array $plugin_info
5697 *
5698 * @throws \Freemius_Exception
5699 */
5700 private function parse_settings( &$plugin_info ) {
5701 $this->_logger->entrance();
5702
5703 $id = $this->get_numeric_option( $plugin_info, 'id', false );
5704 $public_key = $this->get_option( $plugin_info, 'public_key', false );
5705 $secret_key = $this->get_option( $plugin_info, 'secret_key', null );
5706 $parent_id = $this->get_numeric_option( $plugin_info, 'parent_id', null );
5707 $parent_name = $this->get_option( $plugin_info, 'parent_name', null );
5708
5709 /**
5710 * @author Vova Feldman (@svovaf)
5711 * @since 1.1.9 Try to pull secret key from external config.
5712 */
5713 if ( is_null( $secret_key ) && defined( "WP_FS__{$this->_slug}_SECRET_KEY" ) ) {
5714 $secret_key = constant( "WP_FS__{$this->_slug}_SECRET_KEY" );
5715 }
5716
5717 if ( isset( $plugin_info['parent'] ) ) {
5718 $parent_id = $this->get_numeric_option( $plugin_info['parent'], 'id', null );
5719 // $parent_slug = $this->get_option( $plugin_info['parent'], 'slug', null );
5720 // $parent_public_key = $this->get_option( $plugin_info['parent'], 'public_key', null );
5721 // $parent_name = $this->get_option( $plugin_info['parent'], 'name', null );
5722 }
5723
5724 if ( false === $id ) {
5725 throw new Freemius_Exception( array(
5726 'error' => array(
5727 'type' => 'ParameterNotSet',
5728 'message' => 'Plugin id parameter is not set.',
5729 'code' => 'plugin_id_not_set',
5730 'http' => 500,
5731 )
5732 ) );
5733 }
5734 if ( false === $public_key ) {
5735 throw new Freemius_Exception( array(
5736 'error' => array(
5737 'type' => 'ParameterNotSet',
5738 'message' => 'Plugin public_key parameter is not set.',
5739 'code' => 'plugin_public_key_not_set',
5740 'http' => 500,
5741 )
5742 ) );
5743 }
5744
5745 $plugin = ( $this->_plugin instanceof FS_Plugin ) ?
5746 $this->_plugin :
5747 new FS_Plugin();
5748
5749 $premium_suffix = $this->get_option( $plugin_info, 'premium_suffix', '(Premium)' );
5750
5751 $plugin->update( array(
5752 'id' => $id,
5753 'type' => $this->get_option( $plugin_info, 'type', $this->_module_type ),
5754 'public_key' => $public_key,
5755 'slug' => $this->_slug,
5756 'premium_slug' => $this->get_option( $plugin_info, 'premium_slug', "{$this->_slug}-premium" ),
5757 'parent_plugin_id' => $parent_id,
5758 'version' => $this->get_plugin_version(),
5759 'title' => $this->get_plugin_name( $premium_suffix ),
5760 'file' => $this->_plugin_basename,
5761 'is_premium' => $this->get_bool_option( $plugin_info, 'is_premium', true ),
5762 'premium_suffix' => $premium_suffix,
5763 'is_live' => $this->get_bool_option( $plugin_info, 'is_live', true ),
5764 'affiliate_moderation' => $this->get_option( $plugin_info, 'has_affiliation' ),
5765 'bundle_id' => $this->get_option( $plugin_info, 'bundle_id', null ),
5766 'bundle_public_key' => $this->get_option( $plugin_info, 'bundle_public_key', null ),
5767 ) );
5768
5769 if ( $plugin->is_updated() ) {
5770 // Update plugin details.
5771 $this->_plugin = FS_Plugin_Manager::instance( $this->_module_id )->store( $plugin );
5772 }
5773 // Set the secret key after storing the plugin, we don't want to store the key in the storage.
5774 $this->_plugin->secret_key = $secret_key;
5775
5776 /**
5777 * 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).
5778 *
5779 * @author Vova Feldman
5780 * @since 2.4.5
5781 */
5782 if ( $this->is_network_active() && fs_is_network_admin() ) {
5783 if ( isset( $plugin_info['menu_network'] ) &&
5784 is_array( $plugin_info['menu_network'] ) &&
5785 ! empty( $plugin_info['menu_network'] )
5786 ) {
5787 $plugin_info['menu'] = $plugin_info['menu_network'];
5788 }
5789 }
5790
5791 if ( ! isset( $plugin_info['menu'] ) ) {
5792 $plugin_info['menu'] = array();
5793
5794 if ( ! empty( $this->_storage->sdk_last_version ) &&
5795 version_compare( $this->_storage->sdk_last_version, '1.1.2', '<=' )
5796 ) {
5797 // Backward compatibility to 1.1.2
5798 $plugin_info['menu']['slug'] = isset( $plugin_info['menu_slug'] ) ?
5799 $plugin_info['menu_slug'] :
5800 $this->_slug;
5801 }
5802 }
5803
5804 $this->_menu = FS_Admin_Menu_Manager::instance(
5805 $this->_module_id,
5806 $this->_module_type,
5807 $this->get_unique_affix()
5808 );
5809
5810 $this->_menu->init( $plugin_info['menu'], $this->is_addon() );
5811
5812 $this->_has_addons = $this->get_bool_option( $plugin_info, 'has_addons', false );
5813 $this->_has_paid_plans = $this->get_bool_option( $plugin_info, 'has_paid_plans', true );
5814 $this->_has_premium_version = $this->get_bool_option( $plugin_info, 'has_premium_version', $this->_has_paid_plans );
5815 $this->_ignore_pending_mode = $this->get_bool_option( $plugin_info, 'ignore_pending_mode', false );
5816 $this->_is_org_compliant = $this->get_bool_option( $plugin_info, 'is_org_compliant', true );
5817 $this->_is_premium_only = $this->get_bool_option( $plugin_info, 'is_premium_only', false );
5818 if ( $this->_is_premium_only ) {
5819 // If premium only plugin, disable anonymous mode.
5820 $this->_enable_anonymous = false;
5821 $this->_anonymous_mode = false;
5822 } else {
5823 $this->_enable_anonymous = $this->get_bool_option( $plugin_info, 'enable_anonymous', true );
5824 $this->_anonymous_mode = $this->get_bool_option( $plugin_info, 'anonymous_mode', false );
5825 }
5826 $this->_permissions = $this->get_option( $plugin_info, 'permissions', array() );
5827 $this->_is_bundle_license_auto_activation_enabled = $this->get_option( $plugin_info, 'bundle_license_auto_activation', false );
5828
5829 if ( ! empty( $plugin_info['trial'] ) ) {
5830 $this->_trial_days = $this->get_numeric_option(
5831 $plugin_info['trial'],
5832 'days',
5833 // Default to 0 - trial without days specification.
5834 0
5835 );
5836
5837 $this->_is_trial_require_payment = $this->get_bool_option( $plugin_info['trial'], 'is_require_payment', false );
5838 }
5839
5840 $this->_navigation = $this->get_option(
5841 $plugin_info,
5842 'navigation',
5843 $this->is_free_wp_org_theme() ?
5844 self::NAVIGATION_TABS :
5845 self::NAVIGATION_MENU
5846 );
5847 }
5848
5849 /**
5850 * @param string[] $options
5851 * @param string $key
5852 * @param mixed $default
5853 *
5854 * @return bool
5855 */
5856 private function get_option( &$options, $key, $default = false ) {
5857 return ! empty( $options[ $key ] ) ? $options[ $key ] : $default;
5858 }
5859
5860 private function get_bool_option( &$options, $key, $default = false ) {
5861 return isset( $options[ $key ] ) && is_bool( $options[ $key ] ) ? $options[ $key ] : $default;
5862 }
5863
5864 private function get_numeric_option( &$options, $key, $default = false ) {
5865 return isset( $options[ $key ] ) && is_numeric( $options[ $key ] ) ? $options[ $key ] : $default;
5866 }
5867
5868 /**
5869 * Gate keeper.
5870 *
5871 * @author Vova Feldman (@svovaf)
5872 * @since 1.1.7.3
5873 *
5874 * @return bool
5875 */
5876 private function should_stop_execution() {
5877 if ( empty( $this->_storage->was_plugin_loaded ) ) {
5878 /**
5879 * Don't execute Freemius until plugin was fully loaded at least once,
5880 * to give the opportunity for the activation hook to run before pinging
5881 * the API for connectivity test. This logic is relevant for the
5882 * identification of new plugin install vs. plugin update.
5883 *
5884 * @author Vova Feldman (@svovaf)
5885 * @since 1.1.9
5886 */
5887 return true;
5888 }
5889
5890 if ( $this->is_activation_mode() ) {
5891 if ( ! is_admin() ) {
5892 /**
5893 * If in activation mode, don't execute Freemius outside the admin dashboard.
5894 *
5895 * @author Vova Feldman (@svovaf)
5896 * @since 1.1.7.3
5897 */
5898 return true;
5899 }
5900
5901 if ( ! WP_FS__IS_HTTP_REQUEST ) {
5902 /**
5903 * If in activation and executed without HTTP context (e.g. CLI, Cronjob),
5904 * then don't start Freemius.
5905 *
5906 * @author Vova Feldman (@svovaf)
5907 * @since 1.1.6.3
5908 *
5909 * @link https://wordpress.org/support/topic/errors-in-the-freemius-class-when-running-in-wordpress-in-cli
5910 */
5911 return true;
5912 }
5913
5914 if ( self::is_cron() ) {
5915 /**
5916 * If in activation mode, don't execute Freemius during wp crons
5917 * (wp crons have HTTP context - called as HTTP request).
5918 *
5919 * @author Vova Feldman (@svovaf)
5920 * @since 1.1.7.3
5921 */
5922 return true;
5923 }
5924
5925 if ( self::is_ajax() &&
5926 ! $this->_admin_notices->has_sticky( 'failed_connect_api_first' ) &&
5927 ! $this->_admin_notices->has_sticky( 'failed_connect_api' )
5928 ) {
5929 /**
5930 * During activation, if running in AJAX mode, unless there's a sticky
5931 * connectivity issue notice, don't run Freemius.
5932 *
5933 * @author Vova Feldman (@svovaf)
5934 * @since 1.1.7.3
5935 */
5936 return true;
5937 }
5938 }
5939
5940 return false;
5941 }
5942
5943 /**
5944 * Triggered after code type has changed.
5945 *
5946 * @author Vova Feldman (@svovaf)
5947 * @since 1.1.9.1
5948 */
5949 function _after_code_type_change() {
5950 $this->_logger->entrance();
5951
5952 if ( $this->is_theme() ) {
5953 // Expire the cache of the previous tabs since the theme may
5954 // have setting updates after code type has changed.
5955 $this->_cache->expire( 'tabs' );
5956 $this->_cache->expire( 'tabs_stylesheets' );
5957 }
5958
5959 if ( $this->is_registered() ) {
5960 if ( ! $this->is_addon() ) {
5961 add_action(
5962 is_admin() ? 'admin_init' : 'init',
5963 array( &$this, '_plugin_code_type_changed' )
5964 );
5965 }
5966
5967 if ( $this->is_premium() ) {
5968 // Purge cached payments after switching to the premium version.
5969 // @todo This logic doesn't handle purging the cache for serviceware module upgrade.
5970 $this->get_api_user_scope()->purge_cache( "/plugins/{$this->_module_id}/payments.json?include_addons=true" );
5971 }
5972 }
5973 }
5974
5975 /**
5976 * Handles plugin's code type change (free <--> premium).
5977 *
5978 * @author Vova Feldman (@svovaf)
5979 * @since 1.0.9
5980 */
5981 function _plugin_code_type_changed() {
5982 $this->_logger->entrance();
5983
5984 if ( $this->is_premium() ) {
5985 $this->reconnect_locally();
5986
5987 // Activated premium code.
5988 $this->do_action( 'after_premium_version_activation' );
5989
5990 // Remove all sticky messages related to download of the premium version.
5991 $this->_admin_notices->remove_sticky( array(
5992 'trial_started',
5993 'plan_upgraded',
5994 'plan_changed',
5995 'license_activated',
5996 ) );
5997
5998 $notice = '';
5999 if ( ! $this->is_only_premium() ) {
6000 $notice = sprintf( $this->get_text_inline( 'Premium %s version was successfully activated.', 'premium-activated-message' ), $this->_module_type );
6001 }
6002
6003 $license_notice = $this->get_license_network_activation_notice();
6004 if ( ! empty( $license_notice ) ) {
6005 $notice .= ' ' . $license_notice;
6006 }
6007
6008 if ( ! empty( $notice ) ) {
6009 $this->_admin_notices->add_sticky(
6010 trim( $notice ),
6011 'premium_activated',
6012 $this->get_text_x_inline( 'W00t',
6013 'Used to express elation, enthusiasm, or triumph (especially in electronic communication).', 'woot' ) . '!'
6014 );
6015 }
6016 } else {
6017 // Remove sticky message related to premium code activation.
6018 $this->_admin_notices->remove_sticky( 'premium_activated' );
6019
6020 // Activated free code (after had the premium before).
6021 $this->do_action( 'after_free_version_reactivation' );
6022
6023 if ( $this->is_paying() && ! $this->is_premium() ) {
6024 $this->_admin_notices->add_sticky(
6025 sprintf(
6026 /* translators: %s: License type (e.g. you have a professional license) */
6027 $this->get_text_inline( 'You have a %s license.', 'you-have-x-license' ),
6028 $this->get_plan_title()
6029 ) . $this->get_complete_upgrade_instructions(),
6030 'plan_upgraded',
6031 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
6032 );
6033 }
6034 }
6035
6036 // Schedule code type changes event.
6037 $this->schedule_install_sync();
6038
6039 /**
6040 * Unregister the uninstall hook for the other version of the plugin (with different code type) to avoid
6041 * triggering a fatal error when uninstalling that plugin. For example, after deactivating the "free" version
6042 * of a specific plugin, its uninstall hook should be unregistered after the "premium" version has been
6043 * activated. If we don't do that, a fatal error will occur when we try to uninstall the "free" version since
6044 * the main file of the "free" version will be loaded first before calling the hooked callback. Since the
6045 * free and premium versions are almost identical (same class or have same functions), a fatal error like
6046 * "Cannot redeclare class MyClass" or "Cannot redeclare my_function()" will occur.
6047 */
6048 $this->unregister_uninstall_hook();
6049
6050 $this->clear_module_main_file_cache();
6051
6052 // Update is_premium of latest version.
6053 $this->_storage->prev_is_premium = $this->_plugin->is_premium;
6054 }
6055
6056 #endregion
6057
6058 #----------------------------------------------------------------------------------
6059 #region Add-ons
6060 #----------------------------------------------------------------------------------
6061
6062 /**
6063 * Check if add-on installed and activated on site.
6064 *
6065 * @author Vova Feldman (@svovaf)
6066 * @since 1.0.6
6067 *
6068 * @param string|number $id_or_slug
6069 * @param bool|null $is_premium Since 1.2.1.7 can check for specified add-on version.
6070 *
6071 * @return bool
6072 */
6073 function is_addon_activated( $id_or_slug, $is_premium = null ) {
6074 $this->_logger->entrance();
6075
6076 $addon_id = self::get_module_id( $id_or_slug );
6077 $is_activated = self::has_instance( $addon_id );
6078
6079 if ( ! $is_activated ) {
6080 return false;
6081 }
6082
6083 if ( is_bool( $is_premium ) ) {
6084 // Check if the specified code version is activate.
6085 $addon = $this->get_addon_instance( $addon_id );
6086 $is_activated = ( $is_premium === $addon->is_premium() );
6087 }
6088
6089 return $is_activated;
6090 }
6091
6092 /**
6093 * Check if add-on was connected to install
6094 *
6095 * @author Vova Feldman (@svovaf)
6096 * @since 1.1.7
6097 *
6098 * @param string|number $id_or_slug
6099 *
6100 * @return bool
6101 */
6102 function is_addon_connected( $id_or_slug ) {
6103 $this->_logger->entrance();
6104
6105 $sites = self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN );
6106
6107 $addon_id = self::get_module_id( $id_or_slug );
6108 $addon = $this->get_addon( $addon_id );
6109 $slug = $addon->slug;
6110 if ( ! isset( $sites[ $slug ] ) ) {
6111 return false;
6112 }
6113
6114 $site = $sites[ $slug ];
6115
6116 $plugin = FS_Plugin_Manager::instance( $addon_id )->get();
6117
6118 if ( $plugin->parent_plugin_id != $this->_plugin->id ) {
6119 // The given slug do NOT belong to any of the plugin's add-ons.
6120 return false;
6121 }
6122
6123 return ( is_object( $site ) &&
6124 is_numeric( $site->id ) &&
6125 is_numeric( $site->user_id ) &&
6126 FS_Plugin_Plan::is_valid_id( $site->plan_id )
6127 );
6128 }
6129
6130 /**
6131 * Determines if add-on installed.
6132 *
6133 * NOTE: This is a heuristic and only works if the folder/file named as the slug.
6134 *
6135 * @author Vova Feldman (@svovaf)
6136 * @since 1.0.6
6137 *
6138 * @param string|number $id_or_slug
6139 *
6140 * @return bool
6141 */
6142 function is_addon_installed( $id_or_slug ) {
6143 $this->_logger->entrance();
6144
6145 $addon_id = self::get_module_id( $id_or_slug );
6146
6147 return file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $this->get_addon_basename( $addon_id ) ) );
6148 }
6149
6150 /**
6151 * Get add-on basename.
6152 *
6153 * @author Vova Feldman (@svovaf)
6154 * @since 1.0.6
6155 *
6156 * @param string|number $id_or_slug
6157 *
6158 * @return string
6159 */
6160 function get_addon_basename( $id_or_slug ) {
6161 $addon_id = self::get_module_id( $id_or_slug );
6162
6163 if ( $this->is_addon_activated( $addon_id ) ) {
6164 return self::instance( $addon_id )->get_plugin_basename();
6165 }
6166
6167 $addon = $this->get_addon( $addon_id );
6168 $premium_basename = "{$addon->premium_slug}/{$addon->slug}.php";
6169
6170 if ( file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $premium_basename ) ) ) {
6171 return $premium_basename;
6172 }
6173
6174 $all_plugins = $this->get_all_plugins();
6175
6176 foreach ( $all_plugins as $basename => $data ) {
6177 if ( $addon->slug === $data['slug'] ||
6178 $addon->premium_slug === $data['slug']
6179 ) {
6180 return $basename;
6181 }
6182 }
6183
6184 $free_basename = "{$addon->slug}/{$addon->slug}.php";
6185
6186 return $free_basename;
6187 }
6188
6189 /**
6190 * Get installed add-ons instances.
6191 *
6192 * @author Vova Feldman (@svovaf)
6193 * @since 1.0.6
6194 *
6195 * @return Freemius[]
6196 */
6197 function get_installed_addons() {
6198 if ( $this->is_addon() ) {
6199 // Add-on cannot have add-ons.
6200 return array();
6201 }
6202
6203 $installed_addons = array();
6204
6205 foreach ( self::$_instances as $instance ) {
6206 if ( $instance->is_addon_of( $this->_plugin->id ) ) {
6207 $installed_addons[] = $instance;
6208 }
6209 }
6210
6211 return $installed_addons;
6212 }
6213
6214 /**
6215 * Check if any add-ons of the plugin are installed.
6216 *
6217 * @author Leo Fajardo (@leorw)
6218 * @since 1.1.1
6219 *
6220 * @return bool
6221 */
6222 function has_installed_addons() {
6223 if ( ! $this->has_addons() ) {
6224 return false;
6225 }
6226
6227 foreach ( self::$_instances as $instance ) {
6228 if ( $instance->is_addon() && is_object( $instance->_parent_plugin ) ) {
6229 if ( $this->_plugin->id == $instance->_parent_plugin->id ) {
6230 return true;
6231 }
6232 }
6233 }
6234
6235 return false;
6236 }
6237
6238 /**
6239 * Tell Freemius that the current plugin is an add-on.
6240 *
6241 * @author Vova Feldman (@svovaf)
6242 * @since 1.0.6
6243 *
6244 * @param number $parent_plugin_id The parent plugin ID
6245 */
6246 function init_addon( $parent_plugin_id ) {
6247 $this->_plugin->parent_plugin_id = $parent_plugin_id;
6248 }
6249
6250 /**
6251 * @author Vova Feldman (@svovaf)
6252 * @since 1.0.6
6253 *
6254 * @return bool
6255 */
6256 function is_addon() {
6257 return (
6258 isset( $this->_plugin->parent_plugin_id ) &&
6259 is_numeric( $this->_plugin->parent_plugin_id )
6260 );
6261 }
6262
6263 /**
6264 * @author Vova Feldman (@svovaf)
6265 * @since 2.3.2
6266 *
6267 * @param number $parent_product_id
6268 *
6269 * @return bool
6270 */
6271 function is_addon_of( $parent_product_id ) {
6272 return (
6273 $this->is_addon() &&
6274 $parent_product_id == $this->_plugin->parent_plugin_id
6275 );
6276 }
6277
6278 /**
6279 * Deactivate add-on if it's premium only and the user does't have a valid license.
6280 *
6281 * @param bool $is_after_trial_cancel
6282 *
6283 * @return bool If add-on was deactivated.
6284 */
6285 private function deactivate_premium_only_addon_without_license( $is_after_trial_cancel = false ) {
6286 if ( ! $this->has_free_plan() &&
6287 ! $this->has_features_enabled_license() &&
6288 ! $this->_has_premium_license()
6289 ) {
6290 if ( $this->is_registered() ) {
6291 // IF wrapper is turned off because activation_timestamp is currently only stored for plugins (not addons).
6292 // if (empty($this->_storage->activation_timestamp) ||
6293 // (WP_FS__SCRIPT_START_TIME - $this->_storage->activation_timestamp) > 30
6294 // ) {
6295 /**
6296 * @todo When it's first fail, there's no reason to try and re-sync because the licenses were just synced after initial activation.
6297 *
6298 * Retry syncing the user add-on licenses.
6299 */
6300 // Sync licenses.
6301 $this->_sync_licenses();
6302 // }
6303
6304 // Try to activate premium license.
6305 $this->_activate_license( true );
6306 }
6307
6308 if ( ! $this->has_free_plan() &&
6309 ! $this->has_features_enabled_license() &&
6310 ! $this->_has_premium_license()
6311 ) {
6312 // @todo Check if deactivate plugins also call the deactivation hook.
6313
6314 $this->_parent->_admin_notices->add_sticky(
6315 sprintf(
6316 ( $is_after_trial_cancel ?
6317 $this->_parent->get_text_inline(
6318 '%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.',
6319 'addon-trial-cancelled-message'
6320 ) :
6321 $this->_parent->get_text_inline(
6322 '%s is a premium only add-on. You have to purchase a license first before activating the plugin.',
6323 'addon-no-license-message'
6324 )
6325 ),
6326 '<b>' . $this->_plugin->title . '</b>'
6327 ) . ' ' . sprintf(
6328 '<a href="%s" aria-label="%s" class="button button-primary" style="margin-left: 10px; vertical-align: middle;">%s &nbsp;&#10140;</a>',
6329 $this->_parent->addon_url( $this->_slug ),
6330 esc_attr( sprintf( $this->_parent->get_text_inline( 'More information about %s', 'more-information-about-x' ), $this->_plugin->title ) ),
6331 $this->_parent->get_text_inline( 'Purchase License', 'purchase-license' )
6332 ),
6333 'no_addon_license_' . $this->_slug,
6334 ( $is_after_trial_cancel ? '' : $this->_parent->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...' ),
6335 ( $is_after_trial_cancel ? 'success' : 'error' )
6336 );
6337
6338 deactivate_plugins( array( $this->_plugin_basename ), true );
6339
6340 return true;
6341 }
6342 }
6343
6344 return false;
6345 }
6346
6347 #endregion
6348
6349 #----------------------------------------------------------------------------------
6350 #region Sandbox
6351 #----------------------------------------------------------------------------------
6352
6353 /**
6354 * Set Freemius into sandbox mode for debugging.
6355 *
6356 * @author Vova Feldman (@svovaf)
6357 * @since 1.0.4
6358 *
6359 * @param string $secret_key
6360 */
6361 function init_sandbox( $secret_key ) {
6362 $this->_plugin->secret_key = $secret_key;
6363
6364 // Update plugin details.
6365 FS_Plugin_Manager::instance( $this->_module_id )->update( $this->_plugin, true );
6366 }
6367
6368 /**
6369 * Check if running payments in sandbox mode.
6370 *
6371 * @author Vova Feldman (@svovaf)
6372 * @since 1.0.4
6373 *
6374 * @return bool
6375 */
6376 function is_payments_sandbox() {
6377 return ( ! $this->is_live() ) || isset( $this->_plugin->secret_key );
6378 }
6379
6380 #endregion
6381
6382 /**
6383 * Check if running test vs. live plugin.
6384 *
6385 * @author Vova Feldman (@svovaf)
6386 * @since 1.0.5
6387 *
6388 * @return bool
6389 */
6390 function is_live() {
6391 return $this->_plugin->is_live;
6392 }
6393
6394 /**
6395 * Check if super-admin skipped connection for all sites in the network.
6396 *
6397 * @author Vova Feldman (@svovaf)
6398 * @since 2.0.0
6399 */
6400 function is_network_anonymous() {
6401 if ( ! $this->_is_network_active ) {
6402 return false;
6403 }
6404
6405 $is_anonymous_ms = $this->_storage->get( 'is_anonymous_ms' );
6406
6407 if ( empty( $is_anonymous_ms ) ) {
6408 return false;
6409 }
6410
6411 return $is_anonymous_ms['is'];
6412 }
6413
6414 /**
6415 * Check if super-admin opted-in for all sites in the network.
6416 *
6417 * @author Vova Feldman (@svovaf)
6418 * @since 2.0.0
6419 */
6420 function is_network_connected() {
6421 if ( ! $this->_is_network_active ) {
6422 return false;
6423 }
6424
6425 return $this->_storage->get( 'is_network_connected' );
6426 }
6427
6428 /**
6429 * Check if the user skipped connecting the account with Freemius.
6430 *
6431 * @author Vova Feldman (@svovaf)
6432 * @since 1.0.7
6433 *
6434 * @return bool
6435 */
6436 function is_anonymous() {
6437 if ( ! isset( $this->_is_anonymous ) ) {
6438 if ( $this->is_network_anonymous() ) {
6439 $this->_is_anonymous = true;
6440 } else if ( fs_is_network_admin() ) {
6441 /**
6442 * When not-network-anonymous, yet, running in the network admin, consider as anonymous only when ALL non-delegated sites are set to anonymous.
6443 */
6444 $non_delegated_sites = $this->get_non_delegated_blog_ids();
6445
6446 foreach ( $non_delegated_sites as $blog_id ) {
6447 $is_anonymous = $this->_storage->get( 'is_anonymous', false, $blog_id );
6448
6449 if ( empty( $is_anonymous ) || false === $is_anonymous[ 'is' ] ) {
6450 $this->_is_anonymous = false;
6451 break;
6452 }
6453 }
6454
6455 if ( false !== $this->_is_anonymous ) {
6456 $this->_is_anonymous = true;
6457 }
6458 } else {
6459 if ( ! isset( $this->_storage->is_anonymous ) ) {
6460 // Not skipped.
6461 $this->_is_anonymous = false;
6462 } else if ( is_bool( $this->_storage->is_anonymous ) ) {
6463 // For back compatibility, since the variable was boolean before.
6464 $this->_is_anonymous = $this->_storage->is_anonymous;
6465
6466 // Upgrade stored data format to 1.1.3 format.
6467 $this->set_anonymous_mode( $this->_storage->is_anonymous );
6468 } else {
6469 // Version 1.1.3 and later.
6470 $this->_is_anonymous = $this->_storage->is_anonymous['is'];
6471 }
6472 }
6473 }
6474
6475 return $this->_is_anonymous;
6476 }
6477
6478 /**
6479 * Check if the user skipped the connection of a specified site.
6480 *
6481 * @author Vova Feldman (@svovaf)
6482 * @since 2.0.0
6483 *
6484 * @param int $blog_id
6485 *
6486 * @return bool
6487 */
6488 function is_anonymous_site( $blog_id = 0 ) {
6489 if ( $this->is_network_anonymous() ) {
6490 return true;
6491 }
6492
6493 $is_anonymous = $this->_storage->get( 'is_anonymous', false, $blog_id );
6494
6495 if ( empty( $is_anonymous ) ) {
6496 return false;
6497 }
6498
6499 return $is_anonymous['is'];
6500 }
6501
6502 /**
6503 * Check if user connected his account and install pending email activation.
6504 *
6505 * @author Vova Feldman (@svovaf)
6506 * @since 1.0.7
6507 *
6508 * @return bool
6509 */
6510 function is_pending_activation() {
6511 return $this->_storage->get( 'is_pending_activation', false );
6512 }
6513
6514 /**
6515 * @author Leo Fajardo (@leorw)
6516 * @since 2.5.0
6517 */
6518 private function clear_pending_activation_mode() {
6519 // Remove the pending activation sticky notice (if it still exists).
6520 $this->_admin_notices->remove_sticky( 'activation_pending' );
6521
6522 // Clear the plugin's pending activation mode.
6523 unset( $this->_storage->is_pending_activation );
6524 }
6525
6526 /**
6527 * Check if plugin must be WordPress.org compliant.
6528 *
6529 * @since 1.0.7
6530 *
6531 * @return bool
6532 */
6533 function is_org_repo_compliant() {
6534 return $this->_is_org_compliant;
6535 }
6536
6537 #--------------------------------------------------------------------------------
6538 #region WP Cron Common
6539 #--------------------------------------------------------------------------------
6540
6541 /**
6542 * @author Vova Feldman (@svovaf)
6543 * @since 2.0.0
6544 *
6545 * @param string $name Cron name.
6546 *
6547 * @return object
6548 */
6549 private function get_cron_data( $name ) {
6550 $this->_logger->entrance( $name );
6551
6552 /**
6553 * @var object $cron_data
6554 */
6555 return $this->_storage->get( "{$name}_cron", null );
6556 }
6557
6558 /**
6559 * @author Vova Feldman (@svovaf)
6560 * @since 2.0.0
6561 *
6562 * @param string $name Cron name.
6563 */
6564 private function clear_cron_data( $name ) {
6565 $this->_logger->entrance( $name );
6566
6567 $this->_storage->remove( "{$name}_cron" );
6568 }
6569
6570 /**
6571 * @author Vova Feldman (@svovaf)
6572 * @since 2.0.0
6573 *
6574 * @param string $name Cron name.
6575 * @param int $cron_blog_id The cron executing blog ID.
6576 */
6577 private function set_cron_data( $name, $cron_blog_id = 0 ) {
6578 $this->_logger->entrance( $name );
6579
6580 $this->_storage->store( "{$name}_cron", (object) array(
6581 'version' => $this->get_plugin_version(),
6582 'blog_id' => $cron_blog_id,
6583 'sdk_version' => $this->version,
6584 'timestamp' => WP_FS__SCRIPT_START_TIME,
6585 'on' => true,
6586 ) );
6587 }
6588
6589 /**
6590 * Get the cron's executing blog ID.
6591 *
6592 * @author Vova Feldman (@svovaf)
6593 * @since 2.0.0
6594 *
6595 * @param string $name Cron name.
6596 *
6597 * @return int
6598 */
6599 private function get_cron_blog_id( $name ) {
6600 $this->_logger->entrance( $name );
6601
6602 if ( ! is_multisite() ) {
6603 // Not a multisite.
6604 return 0;
6605 }
6606
6607 $cron_data = $this->get_cron_data( $name );
6608
6609 return ( is_object( $cron_data ) && is_numeric( $cron_data->blog_id ) ) ?
6610 $cron_data->blog_id :
6611 0;
6612 }
6613
6614 /**
6615 * @author Vova Feldman (@svovaf)
6616 * @since 2.0.0
6617 *
6618 * @param string $name Cron name.
6619 *
6620 * @return bool
6621 */
6622 private function is_cron_on( $name ) {
6623 $this->_logger->entrance( $name );
6624
6625 /**
6626 * @var object $cron_data
6627 */
6628 $cron_data = $this->get_cron_data( $name );
6629
6630 return ( ! is_null( $cron_data ) && true === $cron_data->on );
6631 }
6632
6633 /**
6634 * Unix timestamp for previous cron execution or false if never executed.
6635 *
6636 * @author Vova Feldman (@svovaf)
6637 * @since 2.0.0
6638 *
6639 * @param string $name Cron name.
6640 *
6641 * @return int|false
6642 */
6643 private function cron_last_execution( $name ) {
6644 $this->_logger->entrance( $name );
6645
6646 return $this->_storage->get( "{$name}_timestamp" );
6647 }
6648
6649 /**
6650 * Set cron execution time to now.
6651 *
6652 * @author Vova Feldman (@svovaf)
6653 * @since 2.0.0
6654 *
6655 * @param string $name Cron name.
6656 */
6657 private function set_cron_execution_timestamp( $name ) {
6658 $this->_logger->entrance( $name );
6659
6660 $this->_storage->store( "{$name}_timestamp", time() );
6661 }
6662
6663 /**
6664 * Sets the keepalive time to now.
6665 *
6666 * @author Leo Fajardo (@leorw)
6667 * @since 2.2.3
6668 *
6669 * @param bool|null $use_network_level_storage
6670 */
6671 private function set_keepalive_timestamp( $use_network_level_storage = null ) {
6672 $this->_logger->entrance();
6673
6674 $this->_storage->store( 'keepalive_timestamp', time(), $use_network_level_storage );
6675 }
6676
6677 /**
6678 * Check if cron was executed in the last $period of seconds.
6679 *
6680 * @author Vova Feldman (@svovaf)
6681 * @since 2.0.0
6682 *
6683 * @param string $name Cron name.
6684 * @param int $period In seconds
6685 *
6686 * @return bool
6687 */
6688 private function is_cron_executed( $name, $period = WP_FS__TIME_24_HOURS_IN_SEC ) {
6689 $this->_logger->entrance( $name );
6690
6691 $last_execution = $this->cron_last_execution( $name );
6692
6693 if ( ! is_numeric( $last_execution ) ) {
6694 return false;
6695 }
6696
6697 return ( $last_execution > ( WP_FS__SCRIPT_START_TIME - $period ) );
6698 }
6699
6700 /**
6701 * WP Cron is executed on a site level. When running in a multisite network environment
6702 * with the network integration activated, for optimization reasons, we are consolidating
6703 * the installs data sync cron to be executed only from a single site.
6704 *
6705 * @author Vova Feldman (@svovaf)
6706 * @since 2.0.0
6707 *
6708 * @param int $except_blog_id Target any except the excluded blog ID.
6709 *
6710 * @return int
6711 */
6712 private function get_cron_target_blog_id( $except_blog_id = 0 ) {
6713 if ( ! is_multisite() ) {
6714 return 0;
6715 }
6716
6717 if ( $this->_is_network_active ) {
6718 $network_install_blog_id = $this->_storage->network_install_blog_id;
6719
6720 if (
6721 is_numeric( $network_install_blog_id ) &&
6722 $except_blog_id != $network_install_blog_id &&
6723 self::is_site_active( $network_install_blog_id )
6724 ) {
6725 // Try to run cron from the main network blog.
6726 $install = $this->get_install_by_blog_id( $network_install_blog_id );
6727
6728 if (
6729 is_object( $install ) &&
6730 $this->is_tracking_allowed( $network_install_blog_id, $install )
6731 ) {
6732 return $network_install_blog_id;
6733 }
6734 }
6735 }
6736
6737 // Get first opted-in blog ID with active tracking.
6738 $installs = $this->get_blog_install_map();
6739 foreach ( $installs as $blog_id => $install ) {
6740 if ( $except_blog_id != $blog_id &&
6741 self::is_site_active( $blog_id ) &&
6742 $this->is_tracking_allowed( $blog_id, $install )
6743 ) {
6744 return $blog_id;
6745 }
6746 }
6747
6748 return 0;
6749 }
6750
6751 /**
6752 * @author Vova Feldman (@svovaf)
6753 * @since 2.0.0
6754 *
6755 * @param string $name Cron name.
6756 * @param string $action_tag Callback action tag.
6757 * @param bool $is_network_clear If set to TRUE, clear sync cron even if there are installs that are still connected.
6758 */
6759 private function clear_cron( $name, $action_tag = '', $is_network_clear = false ) {
6760 $this->_logger->entrance( $name );
6761
6762 if ( ! $this->is_cron_on( $name ) ) {
6763 return;
6764 }
6765
6766 $clear_cron = true;
6767 if ( ! $is_network_clear && $this->_is_network_active ) {
6768 $installs = $this->get_blog_install_map();
6769
6770 foreach ( $installs as $blog_id => $install ) {
6771 /**
6772 * @var FS_Site $install
6773 */
6774 if ( $this->is_tracking_allowed( $blog_id, $install ) ) {
6775 $clear_cron = false;
6776 break;
6777 }
6778 }
6779 }
6780
6781 if ( ! $clear_cron ) {
6782 return;
6783 }
6784
6785 $cron_blog_id = $this->get_cron_blog_id( $name );
6786
6787 $this->clear_cron_data( $name );
6788
6789 if ( 0 < $cron_blog_id ) {
6790 switch_to_blog( $cron_blog_id );
6791 }
6792
6793 if ( empty( $action_tag ) ) {
6794 $action_tag = $name;
6795 }
6796
6797 wp_clear_scheduled_hook( $this->get_action_tag( $action_tag ) );
6798
6799 if ( 0 < $cron_blog_id ) {
6800 restore_current_blog();
6801 }
6802 }
6803
6804 /**
6805 * Unix timestamp for next cron execution or false if not scheduled.
6806 *
6807 * @author Vova Feldman (@svovaf)
6808 * @since 2.0.0
6809 *
6810 * @param string $name Cron name.
6811 * @param string $action_tag Callback action tag.
6812 *
6813 * @return int|false
6814 */
6815 private function get_next_scheduled_cron( $name, $action_tag = '' ) {
6816 $this->_logger->entrance( $name );
6817
6818 if ( ! $this->is_cron_on( $name ) ) {
6819 return false;
6820 }
6821
6822 $cron_blog_id = $this->get_cron_blog_id( $name );
6823
6824 if ( 0 < $cron_blog_id ) {
6825 switch_to_blog( $cron_blog_id );
6826 }
6827
6828 if ( empty( $action_tag ) ) {
6829 $action_tag = $name;
6830 }
6831
6832 $next_scheduled = wp_next_scheduled( $this->get_action_tag( $action_tag ) );
6833
6834 if ( 0 < $cron_blog_id ) {
6835 restore_current_blog();
6836 }
6837
6838 return $next_scheduled;
6839 }
6840
6841 /**
6842 * @author Vova Feldman (@svovaf)
6843 * @since 2.0.0
6844 *
6845 * @param string $name Cron name.
6846 * @param string $action_tag Callback action tag.
6847 * @param string $recurrence 'single' or 'daily'.
6848 * @param int $start_at Defaults to now.
6849 * @param bool $randomize_start If true, schedule first job randomly during the next 12 hours. Otherwise, schedule job to start right away.
6850 * @param int $except_blog_id Target any except the excluded blog ID.
6851 */
6852 private function schedule_cron(
6853 $name,
6854 $action_tag = '',
6855 $recurrence = 'single',
6856 $start_at = WP_FS__SCRIPT_START_TIME,
6857 $randomize_start = true,
6858 $except_blog_id = 0
6859 ) {
6860 $this->_logger->entrance( $name );
6861
6862 $this->clear_cron( $name, $action_tag, true );
6863
6864 $cron_blog_id = $this->get_cron_target_blog_id( $except_blog_id );
6865
6866 if ( is_multisite() && 0 == $cron_blog_id ) {
6867 // Don't schedule cron since couldn't find a target blog.
6868 return;
6869 }
6870
6871 if ( 0 < $cron_blog_id ) {
6872 switch_to_blog( $cron_blog_id );
6873 }
6874
6875 if ( 'daily' === $recurrence ) {
6876 if ( $randomize_start ) {
6877 // Schedule first sync with a random 12 hour time range from now.
6878 $start_at += rand( 0, ( WP_FS__TIME_24_HOURS_IN_SEC / 2 ) );
6879 }
6880
6881 // Schedule daily WP cron.
6882 wp_schedule_event(
6883 $start_at,
6884 'daily',
6885 $this->get_action_tag( $action_tag )
6886 );
6887 } else if ( 'single' === $recurrence ) {
6888 // Schedule single cron.
6889 wp_schedule_single_event(
6890 $start_at,
6891 $this->get_action_tag( $action_tag )
6892 );
6893 }
6894
6895 $this->set_cron_data( $name, $cron_blog_id );
6896
6897 if ( 0 < $cron_blog_id ) {
6898 restore_current_blog();
6899 }
6900 }
6901
6902 /**
6903 * Consolidated cron execution for performance optimization. The max number of API requests is based on the number of unique opted-in users.
6904 * that doesn't halt page loading.
6905 *
6906 * @author Vova Feldman (@svovaf)
6907 * @since 2.0.0
6908 *
6909 * @param string $name Cron name.
6910 * @param callable $callable The function that should be executed.
6911 */
6912 private function execute_cron( $name, $callable ) {
6913 $this->_logger->entrance( $name );
6914
6915 // Store the last time data sync was executed.
6916 $this->set_cron_execution_timestamp( $name );
6917
6918 // Check if API is temporary down.
6919 if ( FS_Api::is_temporary_down() ) {
6920 return;
6921 }
6922
6923 // @todo Add logic that identifies API latency, and reschedule the next background sync randomly between 8-16 hours.
6924
6925 $users_2_blog_ids = array();
6926
6927 if ( ! is_multisite() ) {
6928 // Add dummy blog.
6929 $users_2_blog_ids[0] = array( 0 );
6930 } else {
6931 $installs = $this->get_blog_install_map();
6932 foreach ( $installs as $blog_id => $install ) {
6933 if ( $this->is_tracking_allowed( $blog_id, $install ) ) {
6934 if ( ! isset( $users_2_blog_ids[ $install->user_id ] ) ) {
6935 $users_2_blog_ids[ $install->user_id ] = array();
6936 }
6937
6938 $users_2_blog_ids[ $install->user_id ][] = $blog_id;
6939 }
6940 }
6941 }
6942
6943 $current_blog_id = get_current_blog_id();
6944
6945 foreach ( $users_2_blog_ids as $user_id => $blog_ids ) {
6946 if ( 0 < $blog_ids[0] ) {
6947 $this->switch_to_blog( $blog_ids[0] );
6948 }
6949
6950 call_user_func_array( $callable, array( $blog_ids, ( is_multisite() ? $current_blog_id : null ) ) );
6951
6952 foreach ( $blog_ids as $blog_id ) {
6953 $this->do_action( "after_{$name}_cron", $blog_id );
6954 }
6955 }
6956
6957 if ( is_multisite() ) {
6958 $this->switch_to_blog( $current_blog_id, fs_is_network_admin() ? $this->get_network_install() : null );
6959
6960 $this->do_action( "after_{$name}_cron_multisite" );
6961 }
6962 }
6963
6964 #endregion
6965
6966 #----------------------------------------------------------------------------------
6967 #region Daily Sync Cron
6968 #----------------------------------------------------------------------------------
6969
6970
6971 /**
6972 * @author Vova Feldman (@svovaf)
6973 * @since 2.0.0
6974 *
6975 * @return bool
6976 */
6977 private function is_sync_cron_scheduled() {
6978 return $this->is_cron_on( 'sync' );
6979 }
6980
6981 /**
6982 * Get the sync cron's executing blog ID.
6983 *
6984 * @author Vova Feldman (@svovaf)
6985 * @since 2.0.0
6986 *
6987 * @return int
6988 */
6989 private function get_sync_cron_blog_id() {
6990 return $this->get_cron_blog_id( 'sync' );
6991 }
6992
6993 /**
6994 * @author Vova Feldman (@svovaf)
6995 * @since 1.1.7.3
6996 */
6997 private function run_manual_sync() {
6998 if ( ! $this->is_user_admin() ) {
6999 return;
7000 }
7001
7002 // Run manual sync.
7003 $this->_sync_cron();
7004
7005 // Reschedule next cron to run 24 hours from now (performance optimization).
7006 $this->schedule_sync_cron( time() + WP_FS__TIME_24_HOURS_IN_SEC, false );
7007 }
7008
7009 /**
7010 * Data sync cron job. Replaces the background sync non blocking HTTP request
7011 * that doesn't halt page loading.
7012 *
7013 * @author Vova Feldman (@svovaf)
7014 * @since 1.1.7.3
7015 * @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.
7016 */
7017 function _sync_cron() {
7018 $this->_logger->entrance();
7019
7020 $this->execute_cron( 'sync', array( &$this, '_sync_cron_method' ) );
7021 }
7022
7023 /**
7024 * The actual data sync cron logic.
7025 *
7026 * @author Vova Feldman (@svovaf)
7027 * @since 2.0.0
7028 *
7029 * @param int[] $blog_ids
7030 * @param int|null $current_blog_id @since 2.2.3. This is passed from the `execute_cron` method and used by the
7031 * `_sync_plugin_license` method in order to switch to the previous blog when sending
7032 * updates for a single site in case `execute_cron` has switched to a different blog.
7033 */
7034 function _sync_cron_method( array $blog_ids, $current_blog_id = null ) {
7035 if ( $this->is_registered() ) {
7036 if ( $this->has_paid_plan() ) {
7037 // Initiate background plan sync.
7038 $this->_sync_license( true, false, $current_blog_id );
7039
7040 if ( $this->is_paying() ) {
7041 // Check for premium plugin updates.
7042 $this->check_updates( true );
7043 }
7044 } else {
7045 // Sync install(s) (only if something changed locally).
7046 if ( 1 < count( $blog_ids ) ) {
7047 $this->sync_installs();
7048 } else {
7049 $this->sync_install();
7050 }
7051
7052 $this->maybe_sync_install_user();
7053 }
7054 }
7055 }
7056
7057 /**
7058 * Check if sync was executed in the last $period of seconds.
7059 *
7060 * @author Vova Feldman (@svovaf)
7061 * @since 1.1.7.3
7062 *
7063 * @param int $period In seconds
7064 *
7065 * @return bool
7066 */
7067 private function is_sync_executed( $period = WP_FS__TIME_24_HOURS_IN_SEC ) {
7068 return $this->is_cron_executed( 'sync', $period );
7069 }
7070
7071 /**
7072 * @author Vova Feldman (@svovaf)
7073 * @since 1.1.7.3
7074 *
7075 * @return bool
7076 */
7077 private function is_sync_cron_on() {
7078 return $this->is_cron_on( 'sync' );
7079 }
7080
7081 /**
7082 * @author Leo Fajardo (@leorw)
7083 * @since 2.5.0
7084 */
7085 private function maybe_schedule_sync_cron() {
7086 $next_schedule = $this->next_sync_cron();
7087
7088 // The event is properly scheduled, so no need to reschedule it.
7089 if (
7090 is_numeric( $next_schedule ) &&
7091 $next_schedule > time()
7092 ) {
7093 return;
7094 }
7095
7096 $this->schedule_sync_cron();
7097 }
7098
7099 /**
7100 * @author Vova Feldman (@svovaf)
7101 * @since 1.1.7.3
7102 *
7103 * @param int $start_at Defaults to now.
7104 * @param bool $randomize_start If true, schedule first job randomly during the next 12 hours. Otherwise, schedule job to start right away.
7105 * @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.
7106 */
7107 private function schedule_sync_cron(
7108 $start_at = WP_FS__SCRIPT_START_TIME,
7109 $randomize_start = true,
7110 $except_blog_id = 0
7111 ) {
7112 $this->schedule_cron(
7113 'sync',
7114 'data_sync',
7115 'daily',
7116 $start_at,
7117 $randomize_start,
7118 $except_blog_id
7119 );
7120 }
7121
7122 /**
7123 * Add the actual sync function to the cron job hook.
7124 *
7125 * @author Vova Feldman (@svovaf)
7126 * @since 1.1.7.3
7127 */
7128 private function hook_callback_to_sync_cron() {
7129 $this->add_action( 'data_sync', array( &$this, '_sync_cron' ) );
7130 }
7131
7132 /**
7133 * @author Vova Feldman (@svovaf)
7134 * @since 1.1.7.3
7135 *
7136 * @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.
7137 */
7138 private function clear_sync_cron( $is_network_clear = false ) {
7139 $this->_logger->entrance();
7140
7141 $this->clear_cron( 'sync', 'data_sync', $is_network_clear );
7142 }
7143
7144 /**
7145 * Unix timestamp for next sync cron execution or false if not scheduled.
7146 *
7147 * @author Vova Feldman (@svovaf)
7148 * @since 1.1.7.3
7149 *
7150 * @return int|false
7151 */
7152 function next_sync_cron() {
7153 return $this->get_next_scheduled_cron( 'sync', 'data_sync' );
7154 }
7155
7156 /**
7157 * Unix timestamp for previous sync cron execution or false if never executed.
7158 *
7159 * @author Vova Feldman (@svovaf)
7160 * @since 1.1.7.3
7161 *
7162 * @return int|false
7163 */
7164 function last_sync_cron() {
7165 return $this->cron_last_execution( 'sync' );
7166 }
7167
7168 #endregion Daily Sync Cron ------------------------------------------------------------------
7169
7170 #----------------------------------------------------------------------------------
7171 #region Async Install Sync
7172 #----------------------------------------------------------------------------------
7173
7174 /**
7175 * @author Vova Feldman (@svovaf)
7176 * @since 1.1.7.3
7177 *
7178 * @return bool
7179 */
7180 private function is_install_sync_scheduled() {
7181 return $this->is_cron_on( 'install_sync' );
7182 }
7183
7184 /**
7185 * Get the sync cron's executing blog ID.
7186 *
7187 * @author Vova Feldman (@svovaf)
7188 * @since 2.0.0
7189 *
7190 * @return int
7191 */
7192 private function get_install_sync_cron_blog_id() {
7193 return $this->get_cron_blog_id( 'install_sync' );
7194 }
7195
7196 /**
7197 * Instead of running blocking install sync event, execute non blocking scheduled wp-cron.
7198 *
7199 * @author Vova Feldman (@svovaf)
7200 * @since 1.1.7.3
7201 *
7202 * @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.
7203 */
7204 private function schedule_install_sync( $except_blog_id = 0 ) {
7205 if ( $this->is_clone() ) {
7206 return;
7207 }
7208
7209 $this->schedule_cron( 'install_sync', 'install_sync', 'single', WP_FS__SCRIPT_START_TIME, false, $except_blog_id );
7210 }
7211
7212 /**
7213 * Unix timestamp for previous install sync cron execution or false if never executed.
7214 *
7215 * @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.
7216 *
7217 * @author Vova Feldman (@svovaf)
7218 * @since 1.1.7.3
7219 *
7220 * @return int|false
7221 */
7222 function last_install_sync() {
7223 return $this->cron_last_execution( 'install_sync' );
7224 }
7225
7226 /**
7227 * Unix timestamp for next install sync cron execution or false if not scheduled.
7228 *
7229 * @author Vova Feldman (@svovaf)
7230 * @since 1.1.7.3
7231 *
7232 * @return int|false
7233 */
7234 function next_install_sync() {
7235 return $this->get_next_scheduled_cron( 'install_sync', 'install_sync' );
7236 }
7237
7238 /**
7239 * Add the actual install sync function to the cron job hook.
7240 *
7241 * @author Vova Feldman (@svovaf)
7242 * @since 1.1.7.3
7243 */
7244 private function hook_callback_to_install_sync() {
7245 $this->add_action( 'install_sync', array( &$this, '_run_sync_install' ) );
7246 }
7247
7248 /**
7249 * @author Vova Feldman (@svovaf)
7250 * @since 1.1.7.3
7251 *
7252 * @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.
7253 */
7254 private function clear_install_sync_cron( $is_network_clear = false ) {
7255 $this->_logger->entrance();
7256
7257 $this->clear_cron( 'install_sync', 'install_sync', $is_network_clear );
7258 }
7259
7260 /**
7261 * @author Vova Feldman (@svovaf)
7262 * @since 1.1.7.3
7263 * @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.
7264 */
7265 public function _run_sync_install() {
7266 $this->_logger->entrance();
7267
7268 $this->execute_cron( 'sync', array( &$this, '_sync_install_cron_method' ) );
7269 }
7270
7271 /**
7272 * The actual install(s) sync cron logic.
7273 *
7274 * @author Vova Feldman (@svovaf)
7275 * @since 2.0.0
7276 *
7277 * @param int[] $blog_ids
7278 * @param int|null $current_blog_id
7279 */
7280 function _sync_install_cron_method( array $blog_ids, $current_blog_id = null ) {
7281 if ( $this->is_registered() ) {
7282 if ( 1 < count( $blog_ids ) ) {
7283 $this->sync_installs( array(), true );
7284 } else {
7285 $this->sync_install( array(), true );
7286 }
7287
7288 $this->maybe_sync_install_user();
7289 }
7290 }
7291
7292 #endregion Async Install Sync ------------------------------------------------------------------
7293
7294 /**
7295 * Show a notice that activation is currently pending.
7296 *
7297 * @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.
7298 *
7299 * @author Vova Feldman (@svovaf)
7300 * @since 1.0.7
7301 *
7302 * @param bool|string $email
7303 * @param bool $is_pending_trial Since 1.2.1.5
7304 * @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.
7305 */
7306 function _add_pending_activation_notice(
7307 $email = false,
7308 $is_pending_trial = false,
7309 $is_suspicious_email = false
7310 ) {
7311 if ( ! is_string( $email ) ) {
7312 $current_user = self::_get_current_wp_user();
7313 $email = $current_user->user_email;
7314 }
7315
7316 $this->_admin_notices->add_sticky(
7317 sprintf(
7318 $this->get_text_inline( 'You should receive a confirmation email for %s to your mailbox at %s. Please make sure you click the button in that email to %s.', 'pending-activation-message' ),
7319 '<b>' . $this->get_plugin_name() . '</b>',
7320 '<b>' . $email . '</b>',
7321 ( $is_pending_trial ?
7322 $this->get_text_inline( 'start the trial', 'start-the-trial' ) :
7323 $this->get_text_inline( 'complete the opt-in', 'complete-the-opt-in' ) )
7324 ),
7325 'activation_pending',
7326 'Thanks!'
7327 );
7328 }
7329
7330 /**
7331 * Check if currently in plugin activation.
7332 *
7333 * @author Vova Feldman (@svovaf)
7334 * @since 1.1.4
7335 *
7336 * @return bool
7337 */
7338 function is_plugin_activation() {
7339 $result = get_transient( "fs_{$this->_module_type}_{$this->_slug}_activated" );
7340
7341 return !empty($result);
7342 }
7343
7344 /**
7345 *
7346 * NOTE: admin_menu action executed before admin_init.
7347 *
7348 * @author Vova Feldman (@svovaf)
7349 * @since 1.0.7
7350 */
7351 function _admin_init_action() {
7352 $is_migration = $this->is_migration();
7353
7354 /**
7355 * Automatically redirect to connect/activation page after plugin activation.
7356 *
7357 * @since 1.1.7 Do NOT redirect to opt-in when running in network admin mode.
7358 */
7359 if ( $this->is_plugin_activation() ) {
7360 delete_transient( "fs_{$this->_module_type}_{$this->_slug}_activated" );
7361
7362 if ( isset( $_GET['activate-multi'] ) ) {
7363 /**
7364 * Don't redirect if activating multiple plugins at once (bulk activation).
7365 */
7366 } else if (
7367 self::is_deactivation_snoozed() &&
7368 (
7369 // Either running the free code base.
7370 ! $this->is_premium() ||
7371 // Or if has a free version.
7372 ! $this->is_only_premium() ||
7373 // If premium only, don't redirect if license is activated.
7374 ( $this->is_registered() && ! $this->can_use_premium_code() )
7375 )
7376 ) {
7377 /**
7378 * 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.
7379 */
7380 } else if ( ! $is_migration ) {
7381 $this->_redirect_on_activation_hook();
7382 return;
7383 }
7384 }
7385
7386 if ( $is_migration ) {
7387 return;
7388 }
7389
7390 if ( fs_request_is_action( $this->get_unique_affix() . '_skip_activation' ) ) {
7391 check_admin_referer( $this->get_unique_affix() . '_skip_activation' );
7392
7393 $this->skip_connection( fs_is_network_admin() );
7394
7395 fs_redirect( $this->get_after_activation_url( 'after_skip_url' ) );
7396 }
7397
7398 if ( $this->is_network_activation_mode() &&
7399 fs_request_is_action( $this->get_unique_affix() . '_delegate_activation' )
7400 ) {
7401 check_admin_referer( $this->get_unique_affix() . '_delegate_activation' );
7402
7403 $this->delegate_connection();
7404
7405 fs_redirect( $this->get_after_activation_url( 'after_delegation_url' ) );
7406 }
7407
7408 $this->_add_upgrade_action_link();
7409
7410 if ( ! ( ! $this->_is_network_active && fs_is_network_admin() ) &&
7411 (
7412 ( true === $this->_storage->require_license_activation ) ||
7413 // Not registered nor anonymous.
7414 ( ! $this->is_registered() && ! $this->is_anonymous() ) ||
7415 // OR, network level and in network upgrade mode.
7416 ( fs_is_network_admin() && $this->_is_network_active && $this->is_network_upgrade_mode() )
7417 )
7418 ) {
7419 if ( ! $this->is_pending_activation() ) {
7420 if ( ! $this->is_activation_page() ) {
7421 /**
7422 * If a user visits any other admin page before activating the premium-only theme with a valid
7423 * license, reactivate the previous theme.
7424 *
7425 * @author Leo Fajardo (@leorw)
7426 * @since 1.2.2
7427 */
7428 if ( $this->is_theme() &&
7429 ! $this->has_settings_menu() &&
7430 ! isset( $_REQUEST['fs_action'] ) &&
7431 $this->can_activate_previous_theme()
7432 ) {
7433 if ( $this->is_only_premium() ) {
7434 $this->activate_previous_theme();
7435 return;
7436 }
7437
7438 if ( true === $this->_storage->require_license_activation ) {
7439 $this->_storage->require_license_activation = false;
7440 }
7441 }
7442
7443 if ( ! fs_is_network_admin() &&
7444 $this->is_network_activation_mode() &&
7445 ! $this->is_delegated_connection()
7446 ) {
7447 return;
7448 }
7449
7450 if ( $this->is_plugin_new_install() || $this->is_only_premium() ) {
7451 if ( ! $this->_anonymous_mode &&
7452 ( ! $this->is_addon() || ! $this->_parent->is_anonymous() ) ) {
7453 // Show notice for new plugin installations.
7454 $this->_admin_notices->add(
7455 sprintf(
7456 $this->get_text_inline( 'You are just one step away - %s', 'you-are-step-away' ),
7457 sprintf( '<b><a href="%s">%s</a></b>',
7458 $this->get_activation_url( array(), ! $this->is_delegated_connection() ),
7459 sprintf( $this->get_text_x_inline( 'Complete "%s" Activation Now',
7460 '%s - plugin name. As complete "PluginX" activation now', 'activate-x-now' ), $this->get_plugin_name() )
7461 )
7462 ),
7463 '',
7464 'update-nag'
7465 );
7466 }
7467 } else {
7468 if ( $this->should_add_sticky_optin_notice() ) {
7469 $this->add_sticky_optin_admin_notice();
7470 }
7471
7472 if ( $this->has_filter( 'optin_pointer_element' ) ) {
7473 // Don't show admin nag if plugin update.
7474 wp_enqueue_script( 'wp-pointer' );
7475 wp_enqueue_style( 'wp-pointer' );
7476
7477 $this->_enqueue_connect_essentials();
7478
7479 add_action( 'admin_print_footer_scripts', array(
7480 $this,
7481 '_add_connect_pointer_script'
7482 ) );
7483 }
7484 }
7485 }
7486 }
7487
7488 if ( $this->show_opt_in_on_themes_page() &&
7489 $this->is_activation_page()
7490 ) {
7491 $this->_show_theme_activation_optin_dialog();
7492 }
7493 }
7494 }
7495
7496 /**
7497 * @author Vova Feldman (@svovaf)
7498 * @since 2.0.0
7499 *
7500 * @return bool
7501 */
7502 private function should_add_sticky_optin_notice() {
7503 if ( $this->is_addon() && $this->_parent->is_anonymous() ) {
7504 return false;
7505 }
7506
7507 if ( fs_is_network_admin() ) {
7508 if ( ! $this->_is_network_active ) {
7509 return false;
7510 }
7511
7512 if ( ! $this->is_network_activation_mode() ) {
7513 return false;
7514 }
7515
7516 return ! isset( $this->_storage->sticky_optin_added_ms );
7517 }
7518
7519 if ( ! $this->is_activation_mode() ) {
7520 return false;
7521 }
7522
7523 // If running from a blog admin and delegated the connection.
7524 return ! isset( $this->_storage->sticky_optin_added );
7525 }
7526
7527 /**
7528 * @author Leo Fajardo (@leorw)
7529 * @since 2.0.0
7530 */
7531 private function add_sticky_optin_admin_notice() {
7532 if ( ! $this->_is_network_active || ! fs_is_network_admin() ) {
7533 $this->_storage->sticky_optin_added = true;
7534 } else {
7535 $this->_storage->sticky_optin_added_ms = true;
7536 }
7537
7538 // Show notice for new plugin installations.
7539 $this->_admin_notices->add_sticky(
7540 sprintf(
7541 $this->get_text_inline( 'We made a few tweaks to the %s, %s', 'few-plugin-tweaks' ),
7542 $this->_module_type,
7543 sprintf( '<b><a href="%s">%s</a></b>',
7544 $this->get_activation_url(),
7545 sprintf( $this->get_text_inline( 'Opt in to make "%s" better!', 'optin-x-now' ), $this->get_plugin_name() )
7546 )
7547 ),
7548 'connect_account',
7549 '',
7550 'update-nag'
7551 );
7552 }
7553
7554 /**
7555 * Enqueue connect requires scripts and styles.
7556 *
7557 * @author Vova Feldman (@svovaf)
7558 * @since 1.1.4
7559 */
7560 function _enqueue_connect_essentials() {
7561 wp_enqueue_script( 'jquery' );
7562 wp_enqueue_script( 'json2' );
7563
7564 fs_enqueue_local_script( 'postmessage', 'nojquery.ba-postmessage.min.js' );
7565 fs_enqueue_local_script( 'fs-postmessage', 'postmessage.js' );
7566 }
7567
7568 /**
7569 * Add connect / opt-in pointer.
7570 *
7571 * @author Vova Feldman (@svovaf)
7572 * @since 1.1.4
7573 */
7574 function _add_connect_pointer_script() {
7575 $vars = array( 'id' => $this->_module_id );
7576 $pointer_content = fs_get_template( 'connect.php', $vars );
7577 ?>
7578 <script type="text/javascript">// <![CDATA[
7579 jQuery(document).ready(function ($) {
7580 if ('undefined' !== typeof(jQuery().pointer)) {
7581
7582 var element = <?php echo $this->apply_filters( 'optin_pointer_element', '$("#non_existing_element");' ) ?>;
7583
7584 if (element.length > 0) {
7585 var optin = $(element).pointer($.extend(true, {}, {
7586 content : <?php echo json_encode( $pointer_content ) ?>,
7587 position : {
7588 edge : 'left',
7589 align: 'center'
7590 },
7591 buttons : function () {
7592 // Don't show pointer buttons.
7593 return '';
7594 },
7595 pointerWidth: 482
7596 }, <?php echo $this->apply_filters( 'optin_pointer_options_json', '{}' ) ?>));
7597
7598 <?php
7599 echo $this->apply_filters( 'optin_pointer_execute', "
7600
7601 optin.pointer('open');
7602
7603 // Tag the opt-in pointer with custom class.
7604 $('.wp-pointer #fs_connect')
7605 .parents('.wp-pointer.wp-pointer-top')
7606 .addClass('fs-opt-in-pointer');
7607
7608 ", 'element', 'optin' ) ?>
7609 }
7610 }
7611 });
7612 // ]]></script>
7613 <?php
7614 }
7615
7616 /**
7617 * Return current page's URL.
7618 *
7619 * @author Vova Feldman (@svovaf)
7620 * @since 1.0.7
7621 *
7622 * @return string
7623 */
7624 static function current_page_url() {
7625 $url = 'http';
7626
7627 if ( isset( $_SERVER["HTTPS"] ) ) {
7628 if ( $_SERVER["HTTPS"] == "on" ) {
7629 $url .= "s";
7630 }
7631 }
7632 $url .= "://";
7633 if ( $_SERVER["SERVER_PORT"] != "80" ) {
7634 $url .= $_SERVER["SERVER_NAME"] . ":" . $_SERVER["SERVER_PORT"] . $_SERVER["REQUEST_URI"];
7635 } else {
7636 $url .= $_SERVER["SERVER_NAME"] . $_SERVER["REQUEST_URI"];
7637 }
7638
7639 return esc_url( $url );
7640 }
7641
7642 /**
7643 * Check if the current page is the plugin's main admin settings page.
7644 *
7645 * @author Vova Feldman (@svovaf)
7646 * @since 1.0.7
7647 *
7648 * @return bool
7649 */
7650 function _is_plugin_page() {
7651 return fs_is_plugin_page( $this->_menu->get_raw_slug() ) ||
7652 fs_is_plugin_page( $this->_slug );
7653 }
7654
7655 /* Events
7656 ------------------------------------------------------------------------------------------------------------------*/
7657 /**
7658 * Delete site install from Database.
7659 *
7660 * @author Vova Feldman (@svovaf)
7661 * @since 1.0.1
7662 *
7663 * @param bool $store
7664 * @param int|null $blog_id Since 2.0.0
7665 *
7666 * @return false|int The install ID if deleted. Otherwise, FALSE (when install not exist).
7667 */
7668 function _delete_site( $store = true, $blog_id = null ) {
7669 return self::_delete_site_by_slug( $this->_slug, $this->_module_type, $store, $blog_id );
7670 }
7671
7672 /**
7673 * Delete site install from Database.
7674 *
7675 * @author Vova Feldman (@svovaf)
7676 * @since 1.2.2.7
7677 *
7678 * @param string $slug
7679 * @param string $module_type
7680 * @param bool $store
7681 * @param int|null $blog_id Since 2.0.0
7682 *
7683 * @return false|int The install ID if deleted. Otherwise, FALSE (when install not exist).
7684 */
7685 static function _delete_site_by_slug( $slug, $module_type, $store = true, $blog_id = null ) {
7686 $sites = self::get_all_sites( $module_type, $blog_id );
7687
7688 $install_id = false;
7689
7690 if ( isset( $sites[ $slug ] ) ) {
7691 if ( is_object( $sites[ $slug ] ) ) {
7692 $install_id = $sites[ $slug ]->id;
7693 }
7694
7695 unset( $sites[ $slug ] );
7696
7697 self::set_account_option_by_module( $module_type, 'sites', $sites, $store, $blog_id );
7698 }
7699
7700 return $install_id;
7701 }
7702
7703 /**
7704 * Delete user.
7705 *
7706 * @author Vova Feldman (@svovaf)
7707 * @since 2.0.0
7708 *
7709 * @param number $user_id
7710 * @param bool $store
7711 *
7712 * @return false|int The user ID if deleted. Otherwise, FALSE (when install not exist).
7713 */
7714 private static function delete_user( $user_id, $store = true ) {
7715 $users = self::get_all_users();
7716
7717 if ( ! is_array( $users ) || ! isset( $users[ $user_id ] ) ) {
7718 return false;
7719 }
7720
7721 unset( $users[ $user_id ] );
7722
7723 self::$_accounts->set_option( 'users', $users, $store );
7724
7725 return $user_id;
7726 }
7727
7728 /**
7729 * Delete plugin's plans information.
7730 *
7731 * @param bool $store Flush to Database if true.
7732 * @param bool $keep_associated_plans If set to false, delete all plans, even if a plan is associated with an install.
7733 *
7734 * @author Vova Feldman (@svovaf)
7735 * @since 1.0.9
7736 */
7737 private function _delete_plans( $store = true, $keep_associated_plans = true ) {
7738 $this->_logger->entrance();
7739
7740 $plans = self::get_all_plans( $this->_module_type );
7741
7742 $plans_to_keep = array();
7743
7744 if ( $keep_associated_plans ) {
7745 $plans_ids_to_keep = $this->get_plans_ids_associated_with_installs();
7746 foreach ( $plans_ids_to_keep as $plan_id ) {
7747 $plan = self::_get_plan_by_id( $plan_id );
7748 if ( is_object( $plan ) ) {
7749 $plans_to_keep[] = self::_encrypt_entity( $plan );
7750 }
7751 }
7752 }
7753
7754 if ( ! empty( $plans_to_keep ) ) {
7755 $plans[ $this->_slug ] = $plans_to_keep;
7756 } else {
7757 unset( $plans[ $this->_slug ] );
7758 }
7759
7760 $this->set_account_option( 'plans', $plans, $store );
7761 }
7762
7763 /**
7764 * Delete all plugin licenses.
7765 *
7766 * @author Vova Feldman (@svovaf)
7767 * @since 1.0.9
7768 *
7769 * @param bool $store
7770 */
7771 private function _delete_licenses( $store = true ) {
7772 $this->_logger->entrance();
7773
7774 $all_licenses = self::get_all_licenses();
7775
7776 unset( $all_licenses[ $this->_module_id ] );
7777
7778 self::$_accounts->set_option( 'all_licenses', $all_licenses, $store );
7779 }
7780
7781 /**
7782 * Check if Freemius was added on new plugin installation.
7783 *
7784 * @author Vova Feldman (@svovaf)
7785 * @since 1.1.5
7786 *
7787 * @return bool
7788 */
7789 function is_plugin_new_install() {
7790 return isset( $this->_storage->is_plugin_new_install ) &&
7791 $this->_storage->is_plugin_new_install;
7792 }
7793
7794 /**
7795 * Check if it's the first plugin release that is running Freemius.
7796 *
7797 * @author Vova Feldman (@svovaf)
7798 * @since 1.2.1.5
7799 *
7800 * @return bool
7801 */
7802 function is_first_freemius_powered_version() {
7803 return empty( $this->_storage->plugin_last_version );
7804 }
7805
7806 /**
7807 * @author Leo Fajardo (@leorw)
7808 * @since 1.2.2
7809 *
7810 * @return bool|string
7811 */
7812 private function get_previous_theme_slug() {
7813 return isset( $this->_storage->previous_theme ) ?
7814 $this->_storage->previous_theme :
7815 false;
7816 }
7817
7818 /**
7819 * @author Leo Fajardo (@leorw)
7820 * @since 1.2.2
7821 *
7822 * @return bool
7823 */
7824 private function can_activate_previous_theme() {
7825 return $this->can_activate_theme( $this->get_previous_theme_slug() );
7826 }
7827
7828 /**
7829 * @author Leo Fajardo (@leorw)
7830 * @since 2.5.0
7831 *
7832 * @return bool
7833 */
7834 private function can_activate_theme( $slug ) {
7835 if ( false !== $slug && current_user_can( 'switch_themes' ) ) {
7836 $theme_instance = wp_get_theme( $slug );
7837
7838 return $theme_instance->exists();
7839 }
7840
7841 return false;
7842 }
7843
7844 /**
7845 * @author Leo Fajardo (@leorw)
7846 * @since 1.2.2
7847 */
7848 private function activate_previous_theme() {
7849 switch_theme( $this->get_previous_theme_slug() );
7850 unset( $this->_storage->previous_theme );
7851
7852 global $pagenow;
7853 if ( 'themes.php' === $pagenow ) {
7854 /**
7855 * Refresh the active theme information.
7856 *
7857 * @author Leo Fajardo (@leorw)
7858 * @since 1.2.2
7859 */
7860 fs_redirect( $this->admin_url( $pagenow ) );
7861 }
7862 }
7863
7864 /**
7865 * @author Leo Fajardo (@leorw)
7866 * @since 1.2.2
7867 *
7868 * @return string
7869 */
7870 function get_previous_theme_activation_url() {
7871 if ( ! $this->can_activate_previous_theme() ) {
7872 return '';
7873 }
7874
7875 /**
7876 * Activation URL
7877 *
7878 * @author Leo Fajardo (@leorw)
7879 * @since 1.2.2
7880 */
7881 return wp_nonce_url(
7882 $this->admin_url( 'themes.php?action=activate&stylesheet=' . urlencode( $this->get_previous_theme_slug() ) ),
7883 'switch-theme_' . $this->get_previous_theme_slug()
7884 );
7885 }
7886
7887 /**
7888 * Saves the slug of the previous theme if it still exists so that it can be used by the logic in the opt-in
7889 * form that decides whether to add a close button to the opt-in dialog or not. So after a premium-only theme is
7890 * activated, the close button will appear and will reactivate the previous theme if clicked. If the previous
7891 * theme doesn't exist, then there will be no close button.
7892 *
7893 * @author Leo Fajardo (@leorw)
7894 * @since 1.2.2
7895 *
7896 * @param string $slug_or_name Old theme's slug or name.
7897 * @param bool|WP_Theme $old_theme WP_Theme instance of the old theme if it still exists.
7898 */
7899 function _activate_theme_event_hook( $slug_or_name, $old_theme = false ) {
7900 $this->_storage->previous_theme = ( false !== $old_theme ) ?
7901 $old_theme->get_stylesheet() :
7902 $slug_or_name;
7903
7904 $this->_activate_plugin_event_hook();
7905 }
7906
7907 /**
7908 * Plugin activated hook.
7909 *
7910 * @author Vova Feldman (@svovaf)
7911 * @since 1.0.1
7912 *
7913 * @uses FS_Api
7914 */
7915 function _activate_plugin_event_hook() {
7916 $this->_logger->entrance( 'slug = ' . $this->_slug );
7917
7918 if ( ! $this->is_user_admin() ) {
7919 return;
7920 }
7921
7922 $this->unregister_uninstall_hook();
7923
7924 // Clear API cache on activation.
7925 FS_Api::clear_cache();
7926
7927 $is_premium_version_activation = $this->is_plugin() ?
7928 ( current_filter() !== ( 'activate_' . $this->_free_plugin_basename ) ) :
7929 $this->is_premium();
7930
7931 if ( $is_premium_version_activation && $this->is_pending_activation() ) {
7932 $this->clear_pending_activation_mode();
7933 }
7934
7935 $this->_logger->info( 'Activating ' . ( $is_premium_version_activation ? 'premium' : 'free' ) . ' plugin version.' );
7936
7937 if ( $this->is_plugin() ) {
7938 // This logic is relevant only to plugins since both the free and premium versions of a plugin can be active at the same time.
7939 // 1. If running in the activation of the FREE module, get the basename of the PREMIUM.
7940 // 2. If running in the activation of the PREMIUM module, get the basename of the FREE.
7941 $other_version_basename = $is_premium_version_activation ?
7942 $this->_free_plugin_basename :
7943 $this->premium_plugin_basename();
7944
7945 if ( ! $this->_is_network_active ) {
7946 /**
7947 * Themes are always network activated, but the ACTUAL activation is per site.
7948 *
7949 * During the activation, the plugin isn't yet active, therefore,
7950 * _is_network_active will be set to false even if it's a network level
7951 * activation. So we need to fix that by looking at the is_network_admin() value.
7952 *
7953 * @author Vova Feldman
7954 */
7955 $this->_is_network_active = (
7956 $this->_is_multisite_integrated &&
7957 fs_is_network_admin()
7958 );
7959 }
7960
7961 /**
7962 * If the other module version is active, deactivate it.
7963 *
7964 * is_plugin_active() checks if the plugin is active on the site or the network level and
7965 * deactivate_plugins() deactivates the plugin whether it's activated on the site or network level.
7966 *
7967 * @author Leo Fajardo (@leorw)
7968 * @since 1.2.2
7969 */
7970 if (
7971 is_plugin_active( $other_version_basename ) &&
7972 $this->apply_filters( 'deactivate_on_activation', true )
7973 ) {
7974 deactivate_plugins( $other_version_basename );
7975 }
7976 }
7977
7978 if ( $this->is_registered() ) {
7979 if ( $is_premium_version_activation ) {
7980 $this->reconnect_locally();
7981 }
7982
7983
7984 // Schedule re-activation event and sync.
7985 // $this->sync_install( array(), true );
7986 $this->schedule_install_sync();
7987
7988 // If activating the premium module version, add an admin notice to congratulate for an upgrade completion.
7989 if ( $is_premium_version_activation ) {
7990 $this->_admin_notices->add(
7991 sprintf( $this->get_text_inline( 'The upgrade of %s was successfully completed.', 'successful-version-upgrade-message' ), sprintf( '<b>%s</b>', $this->_plugin->title ) ),
7992 $this->get_text_x_inline( 'W00t',
7993 'Used to express elation, enthusiasm, or triumph (especially in electronic communication).', 'woot' ) . '!'
7994 );
7995 }
7996 } else if ( $this->is_anonymous() ) {
7997 if ( isset( $this->_storage->is_anonymous_ms ) && $this->_storage->is_anonymous_ms['is'] ) {
7998 $plugin_version = $this->_storage->is_anonymous_ms['version'];
7999 $network = true;
8000 } else {
8001 $plugin_version = isset( $this->_storage->is_anonymous ) ?
8002 $this->_storage->is_anonymous['version'] :
8003 null;
8004 $network = false;
8005 }
8006
8007 /**
8008 * Reset "skipped" click cache on the following:
8009 * 1. Freemius DEV mode.
8010 * 2. WordPress DEBUG mode.
8011 * 3. If a plugin and the user skipped the exact same version before.
8012 *
8013 * @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).
8014 *
8015 * @todo 4. If explicitly asked to retry after every activation.
8016 */
8017 if ( WP_FS__DEV_MODE ||
8018 (
8019 ( $this->is_plugin() || ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ) &&
8020 $this->get_plugin_version() == $plugin_version
8021 )
8022 ) {
8023 $this->reset_anonymous_mode( $network );
8024 }
8025 }
8026
8027 $is_trial_or_has_features_enabled_license = ( $this->is_trial() || $this->has_features_enabled_license() );
8028
8029 if ( $this->is_addon() && ! $is_trial_or_has_features_enabled_license ) {
8030 /**
8031 * When activating an add-on, try to also activate a license.
8032 *
8033 * @author Leo Fajardo (@leorw)
8034 * @since 2.3.0
8035 */
8036 if ( ! $this->_is_network_active ) {
8037 $this->maybe_activate_addon_license();
8038 } else {
8039 $this->maybe_network_activate_addon_license();
8040 }
8041
8042 /**
8043 * Avoid redirecting to the license activation screen after automatically activating an add-on license.
8044 *
8045 * @author Leo Fajardo (@leorw)
8046 * @since 2.3.0
8047 */
8048 $is_trial_or_has_features_enabled_license = ( $this->is_trial() || $this->has_features_enabled_license() );
8049
8050 if ( $is_trial_or_has_features_enabled_license && true === $this->_storage->require_license_activation ) {
8051 $this->_storage->require_license_activation = false;
8052 }
8053 }
8054
8055 if (
8056 $is_premium_version_activation &&
8057 (
8058 ( ! $this->is_registered() && $this->is_anonymous() ) ||
8059 (
8060 $this->is_registered() &&
8061 ! $is_trial_or_has_features_enabled_license
8062 )
8063 )
8064 ) {
8065 $this->_storage->require_license_activation = true;
8066 }
8067
8068 if ( ! isset( $this->_storage->is_plugin_new_install ) ) {
8069 /**
8070 * If no previous version of plugin's version exist, it means that it's either
8071 * the first time that the plugin installed on the site, or the plugin was installed
8072 * before but didn't have Freemius integrated.
8073 *
8074 * Since register_activation_hook() do NOT fires on updates since 3.1, and only fires
8075 * on manual activation via the dashboard, is_plugin_activation() is TRUE
8076 * only after immediate activation.
8077 *
8078 * @since 1.1.4
8079 * @link https://make.wordpress.org/core/2010/10/27/plugin-activation-hooks-no-longer-fire-for-updates/
8080 */
8081 $this->_storage->is_plugin_new_install = empty( $this->_storage->plugin_last_version );
8082 }
8083
8084 /**
8085 * Also flush when activating the premium version so that even if Freemius was off before, the API
8086 * connectivity test can be run again.
8087 *
8088 * @author Leo Fajardo (@leorw)
8089 * @since 2.2.3.1
8090 */
8091 $has_api_connectivity = $this->has_api_connectivity( WP_FS__DEV_MODE || $is_premium_version_activation );
8092
8093 if ( ! $this->_anonymous_mode &&
8094 $has_api_connectivity &&
8095 ! $this->_isAutoInstall
8096 ) {
8097 // Store hint that the plugin was just activated to enable auto-redirection to settings.
8098 set_transient( "fs_{$this->_module_type}_{$this->_slug}_activated", true, 60 );
8099 }
8100
8101 /**
8102 * Activation hook is executed after the plugin's main file is loaded, therefore,
8103 * after the plugin was loaded. The logic is located at activate_plugin()
8104 * ./wp-admin/includes/plugin.php.
8105 *
8106 * @author Vova Feldman (@svovaf)
8107 * @since 1.1.9
8108 */
8109 $this->_storage->was_plugin_loaded = true;
8110 }
8111
8112 /**
8113 * @author Leo Fajardo (@leorw)
8114 * @since 2.3.0
8115 */
8116 private function maybe_activate_addon_license() {
8117 $parent_fs = $this->get_parent_instance();
8118
8119 if (
8120 ! is_object( $parent_fs ) ||
8121 ( ! $parent_fs->is_registered() && ! $parent_fs->is_network_registered() )
8122 ) {
8123 // Try to activate a license only if the parent plugin is active and has a valid `install`.
8124 return;
8125 }
8126
8127 $license = $this->get_active_parent_license();
8128 if ( ! is_object( $license ) ) {
8129 return;
8130 }
8131
8132 if (
8133 $this->is_bundle_license_auto_activation_enabled() &&
8134 ! empty( $license->products )
8135 ) {
8136 $this->activate_bundle_license( $license );
8137
8138 return;
8139 }
8140
8141 if ( ! $this->is_registered() ) {
8142 // Opt in with a license key.
8143 $this->opt_in(
8144 $parent_fs->get_current_or_network_user()->email,
8145 false,
8146 false,
8147 $license->secret_key
8148 );
8149 } else {
8150 // Activate the license.
8151 $install = $this->api_site_call(
8152 '/',
8153 'put',
8154 array( 'license_key' => $this->apply_filters( 'license_key', $license->secret_key ) )
8155 );
8156
8157 if ( ! FS_Api::is_api_error( $install ) ) {
8158 $this->_sync_addon_license( $this->get_id(), true );
8159 }
8160 }
8161 }
8162
8163 /**
8164 * @author Leo Fajardo (@leorw)
8165 * @since 2.3.0
8166 *
8167 * @param FS_Plugin_License $license
8168 */
8169 private function maybe_network_activate_addon_license( $license = null ) {
8170 $parent_fs = $this->get_parent_instance();
8171 if ( ! is_object( $parent_fs ) || ( ! $parent_fs->is_registered() && ! $parent_fs->is_network_registered() ) ) {
8172 // Try to activate a license only if the parent plugin is active and has a valid `install`.
8173 return;
8174 }
8175
8176 $license = ( ! is_null( $license ) ) ?
8177 $license :
8178 $this->get_active_parent_license();
8179
8180 if ( ! is_object( $license ) ) {
8181 return;
8182 }
8183
8184 if (
8185 $this->is_bundle_license_auto_activation_enabled() &&
8186 ! empty( $license->products )
8187 ) {
8188 $this->activate_bundle_license( $license );
8189
8190 return;
8191 }
8192
8193 if ( ! $this->is_network_registered() ) {
8194 $sites = $this->get_sites_for_network_level_optin();
8195
8196 if ( count( $sites ) > $license->left() ) {
8197 // If the add-on is network active, try to activate the license only if it can be activated on all sites.
8198 return;
8199 }
8200
8201 // Opt in with a license key.
8202 $this->opt_in(
8203 $parent_fs->get_user()->email,
8204 false,
8205 false,
8206 $license->secret_key,
8207 false,
8208 false,
8209 false,
8210 null,
8211 $sites
8212 );
8213 } else {
8214 $blog_2_install_map = array();
8215 $site_ids = array();
8216
8217 $all_sites = Freemius::get_sites();
8218
8219 foreach ( $all_sites as $site ) {
8220 $blog_id = Freemius::get_site_blog_id( $site );
8221 $install = $this->get_install_by_blog_id( $blog_id );
8222
8223 if ( is_object( $install ) && FS_Plugin_License::is_valid_id( $install->license_id ) ) {
8224 // Skip license activation for installs that are already associated with a license.
8225 continue;
8226 }
8227
8228 if ( is_object( $install ) ) {
8229 $blog_2_install_map[ $blog_id ] = $install;
8230 } else {
8231 $site_ids[] = $blog_id;
8232 }
8233 }
8234
8235 if ( ( count( $blog_2_install_map ) + count( $site_ids ) ) > $license->left() ) {
8236 return;
8237 }
8238
8239 $user = $this->get_current_or_network_user();
8240
8241 if ( ! empty( $blog_2_install_map ) ) {
8242 $result = $this->activate_license_on_many_installs( $user, $license->secret_key, $blog_2_install_map );
8243
8244 if ( true !== $result ) {
8245 return;
8246 }
8247 }
8248
8249 if ( ! empty( $site_ids ) ) {
8250 $this->activate_license_on_many_sites( $user, $license->secret_key, $site_ids );
8251 }
8252 }
8253 }
8254
8255 /**
8256 * 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.
8257 *
8258 * @author Leo Fajardo (@leorw)
8259 * @since 2.4.0
8260 *
8261 * @param FS_Plugin_License $license
8262 * @param array $sites
8263 * @param int $blog_id
8264 */
8265 private function maybe_activate_bundle_license( FS_Plugin_License $license = null, $sites = array(), $blog_id = 0 ) {
8266 if ( ! is_object( $license ) && $this->has_active_valid_license() ) {
8267 $license = $this->_license;
8268 }
8269
8270 if ( ! is_object( $license ) ) {
8271 return;
8272 }
8273
8274 $parent_license = ( ! empty( $license->products ) ) ?
8275 $license :
8276 $this->get_active_parent_license( $license->secret_key );
8277
8278 if ( is_object( $parent_license ) ) {
8279 $this->activate_bundle_license( $parent_license, $sites, $blog_id );
8280 }
8281 }
8282
8283 /**
8284 * Try to activate a bundle license for all the bundle products installed on the site.
8285 * (1) If a child product install already has a license, the bundle license won't be activated.
8286 * (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.
8287 * (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.
8288 *
8289 * @author Leo Fajardo (@leorw)
8290 * @since 2.4.0
8291 *
8292 * @param FS_Plugin_License $license
8293 * @param array $sites
8294 * @param int $current_blog_id
8295 */
8296 private function activate_bundle_license( $license, $sites = array(), $current_blog_id = 0 ) {
8297 $is_network_admin = fs_is_network_admin();
8298
8299 $installs_by_blog_map = array();
8300 $site_info_by_blog_map = array();
8301
8302 /**
8303 * Try to activate the license for all supported products.
8304 *
8305 * @author Leo Fajardo
8306 */
8307 foreach ( $license->products as $product_id ) {
8308 $fs = self::get_instance_by_id( $product_id );
8309
8310 if ( ! is_object( $fs ) ) {
8311 continue;
8312 }
8313
8314 if ( ! $fs->has_paid_plan() ) {
8315 continue;
8316 }
8317
8318 if (
8319 ! $fs->is_addon() &&
8320 ! FS_Plan_Manager::instance()->has_paid_plan( $fs->_plans )
8321 ) {
8322 /**
8323 * The parent product can be free-only but can have its `has_paid_plan` flag set to `true` when
8324 * there is a context bundle.
8325 */
8326 continue;
8327 }
8328
8329 if ( $current_blog_id > 0 ) {
8330 $fs->switch_to_blog( $current_blog_id );
8331 }
8332
8333 if ( $fs->has_active_valid_license() ) {
8334 continue;
8335 }
8336
8337 if ( ! $is_network_admin || $current_blog_id > 0 ) {
8338 if ( $fs->is_network_active() && ! $fs->is_delegated_connection( $current_blog_id ) ) {
8339 // Do not try to activate the license in the site level if the product is network active and the connection was not delegated.
8340 continue;
8341 }
8342 } else {
8343 if ( ! $fs->is_network_active() ) {
8344 // Do not try to activate the license in the network level if the product is not network active.
8345 continue;
8346 }
8347
8348 if ( $fs->is_network_delegated_connection() ) {
8349 // Do not try to activate the license in the network level if the activation has been delegated to site admins.
8350 continue;
8351 }
8352
8353 $has_install_with_license = false;
8354
8355 // 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.
8356 $filtered_sites = array();
8357
8358 if ( empty( $sites ) ) {
8359 $all_sites = self::get_sites();
8360
8361 foreach ( $all_sites as $site ) {
8362 $sites[] = array( 'blog_id' => self::get_site_blog_id( $site ) );
8363 }
8364 } else {
8365 // Populate the map here to avoid calling `$fs->get_site_info( $site );` in the other `for` loop below.
8366 foreach ( $sites as $site ) {
8367 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
8368 continue;
8369 }
8370
8371 $site_info_by_blog_map[ $site['blog_id'] ] = $site;
8372 }
8373 }
8374
8375 foreach ( $sites as $site ) {
8376 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
8377 continue;
8378 }
8379
8380 $blog_id = $site['blog_id'];
8381
8382 if ( ! isset( $installs_by_blog_map[ $blog_id ] ) ) {
8383 $installs_by_blog_map[ $blog_id ] = self::get_all_sites( $fs->get_module_type(), $blog_id );
8384 }
8385
8386 $installs = $installs_by_blog_map[ $blog_id ];
8387 $install = null;
8388
8389 if ( isset( $installs[ $fs->get_slug() ] ) ) {
8390 $install = $installs[ $fs->get_slug() ];
8391
8392 if (
8393 is_object( $install ) &&
8394 (
8395 ! FS_Site::is_valid_id( $install->id ) ||
8396 ! FS_User::is_valid_id( $install->user_id ) ||
8397 ! FS_Plugin_Plan::is_valid_id( $install->plan_id )
8398 )
8399 ) {
8400 $install = null;
8401 }
8402 }
8403
8404 if (
8405 is_object( $install ) &&
8406 FS_Plugin_License::is_valid_id( $install->license_id )
8407 ) {
8408 $has_install_with_license = true;
8409 break;
8410 }
8411
8412 if ( $fs->is_site_delegated_connection( $blog_id ) ) {
8413 // Site activation delegated, don't activate bundle license on the site in the network admin.
8414 continue;
8415 }
8416
8417 if ( ! isset( $site_info_by_blog_map[ $blog_id ] ) ) {
8418 $site_info_by_blog_map[ $blog_id ] = $fs->get_site_info( $site );
8419 }
8420
8421 $filtered_sites[] = $site_info_by_blog_map[ $blog_id ];
8422 }
8423
8424 if ( $has_install_with_license || empty( $filtered_sites ) ) {
8425 // 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.
8426 continue;
8427 }
8428
8429 $sites = $filtered_sites;
8430 }
8431
8432 $fs->activate_migrated_license(
8433 $license->secret_key,
8434 null,
8435 null,
8436 $sites,
8437 ( $current_blog_id > 0 ? $current_blog_id : null )
8438 );
8439 }
8440 }
8441
8442 /**
8443 * Returns a parent license that can be activated for the context product.
8444 *
8445 * @author Leo Fajardo (@leorw)
8446 * @since 2.3.0
8447 *
8448 * @param string|null $license_key
8449 * @param bool $flush
8450 *
8451 * @return FS_Plugin_License
8452 */
8453 function get_active_parent_license( $license_key = null, $flush = true ) {
8454 $parent_licenses_endpoint = "/plugins/{$this->get_id()}/parent_licenses.json?filter=activatable";
8455
8456 $fs = $this;
8457
8458 if ( $this->is_addon() ) {
8459 $parent_instance = $this->get_parent_instance();
8460
8461 if ( is_object( $parent_instance ) && $parent_instance->is_registered() ) {
8462 $fs = $parent_instance;
8463 }
8464 }
8465
8466 $foreign_licenses = $fs->get_foreign_licenses_info(
8467 self::get_all_licenses( $this->get_parent_id() )
8468 );
8469
8470 if ( ! empty ( $foreign_licenses ) ) {
8471 $foreign_licenses = array(
8472 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
8473 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
8474 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
8475 );
8476
8477 $parent_licenses_endpoint = add_query_arg( $foreign_licenses, $parent_licenses_endpoint );
8478 }
8479
8480 $result = $fs->get_current_or_network_user_api_scope()->get( $parent_licenses_endpoint, $flush );
8481
8482 if (
8483 ! $this->is_api_result_object( $result, 'licenses' ) ||
8484 ! is_array( $result->licenses ) ||
8485 empty( $result->licenses )
8486 ) {
8487 return null;
8488 }
8489
8490 $parent_license = null;
8491
8492 if ( empty( $license_key ) ) {
8493 $parent_license = $result->licenses[0];
8494 } else {
8495 foreach ( $result->licenses as $license ) {
8496 if ( $license_key === $license->secret_key ) {
8497 $parent_license = $license;
8498 break;
8499 }
8500 }
8501 }
8502
8503 if ( ! is_null( $parent_license ) ) {
8504 $parent_license = new FS_Plugin_License( $parent_license );
8505 }
8506
8507 return $parent_license;
8508 }
8509
8510 /**
8511 * @author Leo Fajardo (@leorw)
8512 * @since 2.3.0
8513 *
8514 * @return array
8515 */
8516 function get_sites_for_network_level_optin() {
8517 $sites = array();
8518 $all_sites = self::get_sites();
8519
8520 foreach ( $all_sites as $site ) {
8521 $blog_id = self::get_site_blog_id( $site );
8522
8523 if ( ! $this->is_site_delegated_connection( $blog_id ) &&
8524 ! $this->is_installed_on_site( $blog_id )
8525 ) {
8526 $sites[] = $this->get_site_info( $site );
8527 }
8528 }
8529
8530 return $sites;
8531 }
8532
8533 /**
8534 * Delete account.
8535 *
8536 * @author Vova Feldman (@svovaf)
8537 * @since 1.0.3
8538 *
8539 * @param bool $check_user Enforce checking if user have plugins activation privileges.
8540 */
8541 function delete_account_event( $check_user = true ) {
8542 $this->_logger->entrance( 'slug = ' . $this->_slug );
8543
8544 if ( $check_user && ! $this->is_user_admin() ) {
8545 return;
8546 }
8547
8548 $this->do_action( 'before_account_delete' );
8549
8550 // Clear all admin notices.
8551 $this->_admin_notices->clear_all_sticky( false );
8552
8553 $this->_delete_site( false );
8554
8555 $delete_network_common_data = true;
8556
8557 if ( $this->_is_network_active ) {
8558 $installs = $this->get_blog_install_map();
8559
8560 // Don't delete common network data unless no other installs left.
8561 $delete_network_common_data = empty( $installs );
8562 }
8563
8564 if ( $delete_network_common_data ) {
8565 $this->_delete_plans( false );
8566
8567 $this->_delete_licenses( false );
8568
8569 // Delete add-ons related to plugin's account.
8570 $this->_delete_account_addons( false );
8571 }
8572
8573 // @todo Delete plans and licenses of add-ons.
8574
8575 self::$_accounts->store();
8576
8577 /**
8578 * IMPORTANT:
8579 * Clear crons must be executed before clearing all storage.
8580 * Otherwise, the cron will not be cleared.
8581 */
8582 if ( $delete_network_common_data ) {
8583 $this->clear_sync_cron();
8584 }
8585
8586 $this->clear_install_sync_cron();
8587
8588 // Clear all storage data.
8589 $this->_storage->clear_all( true, array(
8590 'is_delegated_connection',
8591 'connectivity_test',
8592 'is_on',
8593 ), false );
8594
8595 // Send delete event.
8596 $this->get_api_site_scope()->call( '/', 'delete' );
8597
8598 $this->do_action( 'after_account_delete' );
8599 }
8600
8601 /**
8602 * Delete network level account.
8603 *
8604 * @author Vova Feldman (@svovaf)
8605 * @since 2.0.0
8606 *
8607 * @param bool $check_user Enforce checking if user have plugins activation privileges.
8608 */
8609 function delete_network_account_event( $check_user = true ) {
8610 $this->_logger->entrance( 'slug = ' . $this->_slug );
8611
8612 if ( $check_user && ! $this->is_user_admin() ) {
8613 return;
8614 }
8615
8616 $this->do_action( 'before_network_account_delete' );
8617
8618 // Clear all admin notices.
8619 $this->_admin_notices->clear_all_sticky();
8620
8621 $this->_delete_plans( false, false );
8622
8623 $this->_delete_licenses( false );
8624
8625 // Delete add-ons related to plugin's account.
8626 $this->_delete_account_addons( false );
8627
8628 // @todo Delete plans and licenses of add-ons.
8629
8630 self::$_accounts->store( true );
8631
8632 /**
8633 * IMPORTANT:
8634 * Clear crons must be executed before clearing all storage.
8635 * Otherwise, the cron will not be cleared.
8636 */
8637 $this->clear_sync_cron( true );
8638 $this->clear_install_sync_cron( true );
8639
8640 $sites = self::get_sites();
8641
8642 $install_ids = array();
8643 foreach ( $sites as $site ) {
8644 $blog_id = self::get_site_blog_id( $site );
8645
8646 if ( $this->is_site_delegated_connection( $blog_id ) ) {
8647 continue;
8648 }
8649
8650 $install_id = $this->_delete_site( true, $blog_id );
8651
8652 // Clear all storage data.
8653 $this->_storage->clear_all( true, array( 'connectivity_test' ), $blog_id );
8654
8655 if ( FS_Site::is_valid_id( $install_id ) ) {
8656 $install_ids[] = $install_id;
8657 }
8658
8659 switch_to_blog( $blog_id );
8660
8661 $this->do_action( 'after_account_delete' );
8662
8663 restore_current_blog();
8664 }
8665
8666 $this->_storage->clear_all( true, array(
8667 'connectivity_test',
8668 'is_on',
8669 ), true );
8670
8671 // Send delete event.
8672 if ( ! empty( $install_ids ) ) {
8673 $result = $this->get_current_or_network_user_api_scope()->call( "/plugins/{$this->_module_id}/installs.json?ids=" . implode( ',', $install_ids ), 'delete' );
8674 }
8675
8676 $this->do_action( 'after_network_account_delete' );
8677 }
8678
8679 /**
8680 * Plugin deactivation hook.
8681 *
8682 * @author Vova Feldman (@svovaf)
8683 * @since 1.0.1
8684 */
8685 function _deactivate_plugin_hook() {
8686 $this->_logger->entrance( 'slug = ' . $this->_slug );
8687
8688 if ( ! $this->is_user_admin() ) {
8689 return;
8690 }
8691
8692 $is_network_deactivation = fs_is_network_admin();
8693 $storage_keys_for_removal = array();
8694
8695 $this->_admin_notices->clear_all_sticky();
8696
8697 $storage_keys_for_removal[] = 'sticky_optin_added';
8698 if ( isset( $this->_storage->sticky_optin_added ) ) {
8699 unset( $this->_storage->sticky_optin_added );
8700 }
8701
8702 if ( ! isset( $this->_storage->is_plugin_new_install ) ) {
8703 // Remember that plugin was already installed.
8704 $this->_storage->is_plugin_new_install = false;
8705 }
8706
8707 // Hook to plugin uninstall.
8708 register_uninstall_hook( $this->_plugin_main_file_path, array( 'Freemius', '_uninstall_plugin_hook' ) );
8709
8710 $this->clear_module_main_file_cache();
8711 $this->clear_sync_cron( $this->_is_network_active );
8712 $this->clear_install_sync_cron();
8713
8714 if ( $this->is_registered() ) {
8715 if ( $this->is_premium() && ! $this->has_active_valid_license() ) {
8716 FS_Plugin_Updater::instance( $this )->delete_update_data();
8717 }
8718
8719 if ( $is_network_deactivation ) {
8720 // Send deactivation event.
8721 $this->sync_installs( array(
8722 'is_active' => false,
8723 ) );
8724 } else {
8725 // Send deactivation event.
8726 $this->sync_install( array(
8727 'is_active' => false,
8728 ) );
8729 }
8730 } else {
8731 if ( ! $this->has_api_connectivity() ) {
8732 // Reset connectivity test cache.
8733 unset( $this->_storage->connectivity_test );
8734
8735 $storage_keys_for_removal[] = 'connectivity_test';
8736 }
8737 }
8738
8739 if ( $is_network_deactivation ) {
8740 if ( isset( $this->_storage->sticky_optin_added_ms ) ) {
8741 unset( $this->_storage->sticky_optin_added_ms );
8742 }
8743
8744 if ( ! empty( $storage_keys_for_removal ) ) {
8745 $sites = self::get_sites();
8746
8747 foreach ( $sites as $site ) {
8748 $blog_id = self::get_site_blog_id( $site );
8749
8750 foreach ( $storage_keys_for_removal as $key ) {
8751 $this->_storage->remove( $key, false, $blog_id );
8752 }
8753
8754 $this->_storage->save( $blog_id );
8755 }
8756 }
8757 }
8758
8759 // Clear API cache on deactivation.
8760 FS_Api::clear_cache();
8761
8762 $this->remove_sdk_reference();
8763 }
8764
8765 /**
8766 * @author Vova Feldman (@svovaf)
8767 * @since 1.1.6
8768 */
8769 private function remove_sdk_reference() {
8770 global $fs_active_plugins;
8771
8772 foreach ( $fs_active_plugins->plugins as $sdk_path => $data ) {
8773 if ( $this->_plugin_basename == $data->plugin_path ) {
8774 unset( $fs_active_plugins->plugins[ $sdk_path ] );
8775 break;
8776 }
8777 }
8778
8779 fs_fallback_to_newest_active_sdk();
8780 }
8781
8782 /**
8783 * @author Vova Feldman (@svovaf)
8784 * @since 1.1.3
8785 *
8786 * @param bool $is_anonymous
8787 * @param bool|int $network_or_blog_id Since 2.0.0
8788 */
8789 private function set_anonymous_mode( $is_anonymous = true, $network_or_blog_id = 0 ) {
8790 // Store information regarding skip to try and opt-in the user
8791 // again in the future.
8792 $skip_info = array(
8793 'is' => $is_anonymous,
8794 'timestamp' => WP_FS__SCRIPT_START_TIME,
8795 'version' => $this->get_plugin_version(),
8796 );
8797
8798 if ( true === $network_or_blog_id ) {
8799 $this->_storage->is_anonymous_ms = $skip_info;
8800 } else {
8801 $this->_storage->store( 'is_anonymous', $skip_info, $network_or_blog_id );
8802 }
8803
8804 $this->network_upgrade_mode_completed();
8805
8806 // Update anonymous mode cache.
8807 $this->_is_anonymous = $is_anonymous;
8808 }
8809
8810 /**
8811 * @author Vova Feldman (@svovaf)
8812 * @since 2.5.1
8813 *
8814 * @param bool|int $network_or_blog_id
8815 */
8816 private function unset_anonymous_mode( $network_or_blog_id = 0 ) {
8817 if ( true === $network_or_blog_id ) {
8818 unset( $this->_storage->is_anonymous_ms );
8819 } else {
8820 $this->_storage->remove( 'is_anonymous', true, $network_or_blog_id );
8821 }
8822 }
8823
8824 /**
8825 * @author Vova Feldman (@svovaf)
8826 * @since 2.0.0
8827 *
8828 * @param int $blog_id Site ID.
8829 * @param int $user_id User ID.
8830 * @param string $domain Site domain.
8831 * @param string $path Site path.
8832 * @param int $network_id Network ID. Only relevant on multi-network installations.
8833 * @param array $meta Metadata. Used to set initial site options.
8834 *
8835 * @uses Freemius::is_license_network_active() to check if the context license was network activated by the super-admin.
8836 * @uses Freemius::is_network_connected() to check if the super-admin network opted-in.
8837 * @uses Freemius::is_network_anonymous() to check if the super-admin network skipped.
8838 * @uses Freemius::is_network_delegated_connection() to check if the super-admin network delegated the connection to the site admins.
8839 */
8840 public function _after_new_blog_callback( $blog_id, $user_id, $domain, $path, $network_id, $meta ) {
8841 $this->_logger->entrance();
8842
8843 if ( ! $this->_is_network_active ) {
8844 FS_Clone_Manager::instance()->store_blog_install_info( $blog_id );
8845 return;
8846 }
8847
8848 $site = null;
8849 $new_blog_id = $blog_id;
8850
8851 if ( $this->is_premium() &&
8852 $this->is_network_connected() &&
8853 is_object( $this->_license ) &&
8854 $this->_license->can_activate( FS_Site::is_localhost_by_address( $domain ) ) &&
8855 $this->is_license_network_active( $blog_id )
8856 ) {
8857 /**
8858 * 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.
8859 */
8860 $current_blog_id = get_current_blog_id();
8861 $license = clone $this->_license;
8862
8863 $this->switch_to_blog( $blog_id );
8864
8865 // Opt-in with network user.
8866 $this->install_with_user(
8867 $this->get_network_user(),
8868 $license->secret_key,
8869 false,
8870 false,
8871 false
8872 );
8873
8874 if ( is_object( $this->_site ) ) {
8875 if ( $this->_site->license_id == $license->id ) {
8876 /**
8877 * If the license was activated successfully, sync the license data from the remote server.
8878 */
8879 $this->_license = $license;
8880 $this->sync_site_license();
8881 }
8882 }
8883
8884 $site = $this->_site;
8885
8886 $this->switch_to_blog( $current_blog_id );
8887
8888 if ( is_object( $site ) ) {
8889 FS_Clone_Manager::instance()->store_blog_install_info( $blog_id, $site );
8890
8891 // Already connected (with or without a license), so no need to continue.
8892 return;
8893 }
8894 }
8895
8896 if ( $this->is_network_anonymous() ) {
8897 /**
8898 * Opt-in was network skipped so automatically skip the opt-in for the new site.
8899 */
8900 $this->skip_site_connection( $blog_id );
8901 } else if ( $this->is_network_delegated_connection() ) {
8902 /**
8903 * Opt-in was network delegated so automatically delegate the opt-in for the new site's admin.
8904 */
8905 $this->delegate_site_connection( $blog_id );
8906 } else if ( $this->is_network_connected() ) {
8907 /**
8908 * Opt-in was network activated so automatically opt-in with the network user and new site admin.
8909 */
8910 $current_blog_id = get_current_blog_id();
8911
8912 $this->switch_to_blog( $blog_id );
8913
8914 // Opt-in with network user.
8915 $this->install_with_user(
8916 $this->get_network_user(),
8917 false,
8918 false,
8919 false,
8920 false
8921 );
8922
8923 $site = $this->_site;
8924
8925 $this->switch_to_blog( $current_blog_id );
8926 } else {
8927 /**
8928 * If the super-admin mixed different options (connect, skip, delegated):
8929 * a) If at least one site connection was delegated, then automatically delegate connection.
8930 * 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.
8931 */
8932 $has_delegated_site = false;
8933
8934 $sites = self::get_sites();
8935 foreach ( $sites as $wp_site ) {
8936 $blog_id = self::get_site_blog_id( $wp_site );
8937
8938 if ( $this->is_site_delegated_connection( $blog_id ) ) {
8939 $has_delegated_site = true;
8940 break;
8941 }
8942 }
8943
8944 if ( $has_delegated_site ) {
8945 $this->delegate_site_connection( $blog_id );
8946 } else {
8947 $this->skip_site_connection( $blog_id );
8948 }
8949 }
8950
8951 /**
8952 * 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.
8953 *
8954 * @author Leo Fajardo (@leorw)
8955 * @since 2.5.0
8956 */
8957 FS_Clone_Manager::instance()->store_blog_install_info( $new_blog_id, $site );
8958 }
8959
8960 /**
8961 * @author Vova Feldman (@svovaf)
8962 * @since 2.5.0
8963 *
8964 * @param \WP_Site $new_site
8965 * @param array $args
8966 */
8967 public function _after_wp_initialize_site_callback( WP_Site $new_site, $args ) {
8968 $this->_logger->entrance();
8969
8970 $this->_after_new_blog_callback(
8971 $new_site->id,
8972 // Dummy user ID (not in use).
8973 0,
8974 $new_site->domain,
8975 $new_site->path,
8976 $new_site->network_id,
8977 // Dummy meta, not in use.
8978 array()
8979 );
8980 }
8981
8982 /**
8983 * @author Vova Feldman (@svovaf)
8984 * @since 1.1.3
8985 *
8986 * @param bool|int|int[] $network_or_blog_ids Since 2.0.0.
8987 */
8988 private function reset_anonymous_mode( $network_or_blog_ids = false ) {
8989 if ( true === $network_or_blog_ids ) {
8990 $this->unset_anonymous_mode( true );
8991
8992 if ( fs_is_network_admin() ) {
8993 $this->_is_anonymous = null;
8994 }
8995
8996 // Rest anonymous mode for all non-delegated sub-sites.
8997 $blog_ids = $this->get_non_delegated_blog_ids();
8998 }
8999 else
9000 {
9001 if ( false === $network_or_blog_ids ) {
9002 $network_or_blog_ids = 0;
9003 }
9004
9005 $blog_ids = is_array( $network_or_blog_ids ) ?
9006 $network_or_blog_ids :
9007 array( $network_or_blog_ids );
9008
9009 foreach ( $blog_ids as $blog_id ) {
9010 if ( 0 === $blog_id || get_current_blog_id() == $blog_id ) {
9011 $this->_is_anonymous = null;
9012 }
9013 }
9014 }
9015
9016 foreach ( $blog_ids as $blog_id ) {
9017 $this->unset_anonymous_mode( $blog_id );
9018 }
9019
9020 /**
9021 * Ensure that this field is also "false", otherwise, if the current module's type is "theme" and the module
9022 * has no menus, the opt-in popup will not be shown immediately (in this case, the user will have to click
9023 * on the admin notice that contains the opt-in link in order to trigger the opt-in popup).
9024 *
9025 * @author Leo Fajardo (@leorw)
9026 * @since 1.2.2
9027 */
9028 if ( ! $this->_is_network_active ) {
9029 $this->_is_anonymous = null;
9030 }
9031 }
9032
9033 /**
9034 * This is used to ensure that before redirecting to the opt-in page after resetting the anonymous mode or
9035 * deleting the account in the network level, the URL of the page to redirect to is correct.
9036 *
9037 * @author Leo Fajardo (@leorw)
9038 *
9039 * @since 2.1.3
9040 */
9041 private function maybe_set_slug_and_network_menu_exists_flag() {
9042 if ( ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
9043 $this->_menu->set_slug_and_network_menu_exists_flag( $this->_menu->has_menu() ?
9044 $this->_menu->get_slug() :
9045 $this->_slug
9046 );
9047 }
9048 }
9049
9050 /**
9051 * Clears the anonymous mode and redirects to the opt-in screen.
9052 *
9053 * @author Vova Feldman (@svovaf)
9054 * @since 1.1.7
9055 */
9056 function connect_again() {
9057 if ( ! $this->is_anonymous() ) {
9058 return;
9059 }
9060
9061 $this->reset_anonymous_mode( fs_is_network_admin() );
9062
9063 $this->maybe_set_slug_and_network_menu_exists_flag();
9064
9065 fs_redirect( $this->get_activation_url() );
9066 }
9067
9068 /**
9069 * Skip account connect, and set anonymous mode.
9070 *
9071 * @author Vova Feldman (@svovaf)
9072 * @since 1.1.1
9073 *
9074 * @param bool|int|int[] $network_or_blog_ids Since 2.5.1
9075 */
9076 function skip_connection( $network_or_blog_ids = false ) {
9077 $this->_logger->entrance();
9078
9079 $this->_admin_notices->remove_sticky( 'connect_account' );
9080
9081 if ( true === $network_or_blog_ids ) {
9082 $this->set_anonymous_mode( true, true );
9083
9084 if ( fs_is_network_admin() ) {
9085 $this->_is_anonymous = null;
9086 }
9087
9088 // Rest anonymous mode for all non-delegated sub-sites.
9089 $blog_ids = $this->get_non_delegated_blog_ids();
9090 }
9091 else
9092 {
9093 if ( false === $network_or_blog_ids ) {
9094 $network_or_blog_ids = 0;
9095 }
9096
9097 $blog_ids = is_array( $network_or_blog_ids ) ?
9098 $network_or_blog_ids :
9099 array( $network_or_blog_ids );
9100
9101 foreach ( $blog_ids as $blog_id ) {
9102 if ( 0 === $blog_id || get_current_blog_id() == $blog_id ) {
9103 $this->_is_anonymous = null;
9104 }
9105 }
9106 }
9107
9108 foreach ( $blog_ids as $blog_id ) {
9109 $this->skip_site_connection( $blog_id );
9110 }
9111
9112 $this->network_upgrade_mode_completed();
9113 }
9114
9115 /**
9116 * Skip connection for specific site in the network.
9117 *
9118 * @author Vova Feldman (@svovaf)
9119 * @since 2.0.0
9120 *
9121 * @param int|null $blog_id
9122 * @param bool $send_skip
9123 */
9124 private function skip_site_connection( $blog_id = null ) {
9125 $this->_logger->entrance();
9126
9127 $this->_admin_notices->remove_sticky( 'connect_account', $blog_id );
9128
9129 $this->set_anonymous_mode( true, $blog_id );
9130 }
9131
9132 /**
9133 * Plugin version update hook.
9134 *
9135 * @author Vova Feldman (@svovaf)
9136 * @since 1.0.4
9137 */
9138 private function update_plugin_version_event() {
9139 $this->_logger->entrance();
9140
9141 if ( ! $this->is_registered() ) {
9142 return;
9143 }
9144
9145 $this->schedule_install_sync();
9146 // $this->sync_install( array(), true );
9147 }
9148
9149 /**
9150 * Generate an MD5 signature of a plugins collection.
9151 * This helper methods used to identify changes in a plugins collection.
9152 *
9153 * @author Vova Feldman (@svovaf)
9154 * @since 2.0.0
9155 *
9156 * @param array [string]array $plugins
9157 *
9158 * @return string
9159 */
9160 private function get_plugins_thumbprint( $plugins ) {
9161 ksort( $plugins );
9162
9163 $thumbprint = '';
9164 foreach ( $plugins as $basename => $data ) {
9165 $thumbprint .= $data['slug'] . ',' .
9166 $data['Version'] . ',' .
9167 ( $data['is_active'] ? '1' : '0' ) . ';';
9168 }
9169
9170 return md5( $thumbprint );
9171 }
9172
9173 /**
9174 * Return a list of modified plugins since the last sync.
9175 *
9176 * Note:
9177 * There's no point to store a plugins counter since even if the number of
9178 * plugins didn't change, we still need to check if the versions are all the
9179 * same and the activity state is similar.
9180 *
9181 * @author Vova Feldman (@svovaf)
9182 * @since 1.1.8
9183 *
9184 * @return array|false
9185 */
9186 private function get_plugins_data_for_api() {
9187 // Alias.
9188 $site_active_plugins_option_name = 'active_plugins';
9189 $network_plugins_option_name = 'all_plugins';
9190
9191 /**
9192 * Collection of all site level active plugins.
9193 */
9194 $site_active_plugins_cache = self::$_accounts->get_option( $site_active_plugins_option_name );
9195
9196 if ( ! is_object( $site_active_plugins_cache ) ) {
9197 $site_active_plugins_cache = (object) array(
9198 'timestamp' => '',
9199 'md5' => '',
9200 'plugins' => array(),
9201 );
9202 }
9203
9204 $time = time();
9205
9206 if ( ! empty( $site_active_plugins_cache->timestamp ) &&
9207 ( $time - $site_active_plugins_cache->timestamp ) < WP_FS__TIME_5_MIN_IN_SEC
9208 ) {
9209 // Don't send plugin updates if last update was in the past 5 min.
9210 return false;
9211 }
9212
9213 // Write timestamp to lock the logic.
9214 $site_active_plugins_cache->timestamp = $time;
9215 self::$_accounts->set_option( $site_active_plugins_option_name, $site_active_plugins_cache, true );
9216
9217 // Reload options from DB.
9218 self::$_accounts->load( true );
9219 $site_active_plugins_cache = self::$_accounts->get_option( $site_active_plugins_option_name );
9220
9221 if ( $time != $site_active_plugins_cache->timestamp ) {
9222 // If timestamp is different, then another thread captured the lock.
9223 return false;
9224 }
9225
9226 /**
9227 * Collection of all plugins (network level).
9228 */
9229 $network_plugins_cache = self::$_accounts->get_option( $network_plugins_option_name );
9230
9231 if ( ! is_object( $network_plugins_cache ) ) {
9232 $network_plugins_cache = (object) array(
9233 'timestamp' => '',
9234 'md5' => '',
9235 'plugins' => array(),
9236 );
9237 }
9238
9239 // Check if there's a change in plugins.
9240 $network_plugins = self::get_network_plugins();
9241 $site_active_plugins = self::get_site_active_plugins();
9242
9243 $network_plugins_thumbprint = $this->get_plugins_thumbprint( $network_plugins );
9244 $site_active_plugins_thumbprint = $this->get_plugins_thumbprint( $site_active_plugins );
9245
9246 // Check if plugins status changed (version or active/inactive).
9247 $network_plugins_changed = ( $network_plugins_cache->md5 !== $network_plugins_thumbprint );
9248 $site_active_plugins_changed = ( $site_active_plugins_cache->md5 !== $site_active_plugins_thumbprint );
9249
9250 if ( ! $network_plugins_changed &&
9251 ! $site_active_plugins_changed
9252 ) {
9253 // No changes.
9254 return array();
9255 }
9256
9257 $plugins_update_data = array();
9258
9259 foreach ( $network_plugins_cache->plugins as $basename => $data ) {
9260 if ( ! isset( $network_plugins[ $basename ] ) ) {
9261 // Plugin uninstalled.
9262 $uninstalled_plugin_data = $data;
9263 $uninstalled_plugin_data['is_active'] = false;
9264 $uninstalled_plugin_data['is_uninstalled'] = true;
9265 $plugins_update_data[] = $uninstalled_plugin_data;
9266
9267 unset( $network_plugins[ $basename ] );
9268
9269 unset( $network_plugins_cache->plugins[ $basename ] );
9270 unset( $site_active_plugins_cache->plugins[ $basename ] );
9271
9272 continue;
9273 }
9274
9275 $was_active = $data['is_active'] ||
9276 ( isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
9277 true === $site_active_plugins_cache->plugins[ $basename ]['is_active'] );
9278 $is_active = $network_plugins[ $basename ]['is_active'] ||
9279 ( isset( $site_active_plugins[ $basename ] ) &&
9280 $site_active_plugins[ $basename ]['is_active'] );
9281
9282 if ( ! isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
9283 isset( $site_active_plugins[ $basename ] )
9284 ) {
9285 // Plugin was site level activated.
9286 $site_active_plugins_cache->plugins[ $basename ] = $network_plugins[ $basename ];
9287 $site_active_plugins_cache->plugins[ $basename ]['is_active'] = true;
9288 } else if ( isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
9289 ! isset( $site_active_plugins[ $basename ] )
9290 ) {
9291 // Plugin was site level deactivated.
9292 unset( $site_active_plugins_cache->plugins[ $basename ] );
9293 }
9294
9295 $prev_version = $data['version'];
9296 $current_version = $network_plugins[ $basename ]['Version'];
9297
9298 if ( $was_active !== $is_active || $prev_version !== $current_version ) {
9299 // Plugin activated or deactivated, or version changed.
9300
9301 if ( $was_active !== $is_active ) {
9302 if ( $data['is_active'] != $network_plugins[ $basename ]['is_active'] ) {
9303 $network_plugins_cache->plugins[ $basename ]['is_active'] = $data['is_active'];
9304 }
9305 }
9306
9307 if ( $prev_version !== $current_version ) {
9308 $network_plugins_cache->plugins[ $basename ]['Version'] = $current_version;
9309 }
9310
9311 $updated_plugin_data = $data;
9312 $updated_plugin_data['is_active'] = $is_active;
9313 $updated_plugin_data['version'] = $current_version;
9314 $updated_plugin_data['title'] = $network_plugins[ $basename ]['Name'];
9315 $plugins_update_data[] = $updated_plugin_data;
9316 }
9317 }
9318
9319 // Find new plugins that weren't yet seen before.
9320 foreach ( $network_plugins as $basename => $data ) {
9321 if ( ! isset( $network_plugins_cache->plugins[ $basename ] ) ) {
9322 // New plugin.
9323 $new_plugin = array(
9324 'slug' => $data['slug'],
9325 'version' => $data['Version'],
9326 'title' => $data['Name'],
9327 'is_active' => $data['is_active'],
9328 'is_uninstalled' => false,
9329 );
9330
9331 $network_plugins_cache->plugins[ $basename ] = $new_plugin;
9332
9333 $is_site_level_active = (
9334 isset( $site_active_plugins[ $basename ] ) &&
9335 $site_active_plugins[ $basename ]['is_active']
9336 );
9337
9338 /**
9339 * If not network active, set the activity status based on the site-level plugin status.
9340 */
9341 if ( ! $new_plugin['is_active'] ) {
9342 $new_plugin['is_active'] = $is_site_level_active;
9343 }
9344
9345 $plugins_update_data[] = $new_plugin;
9346
9347 if ( isset( $site_active_plugins[ $basename ] ) ) {
9348 $site_active_plugins_cache->plugins[ $basename ] = $new_plugin;
9349 $site_active_plugins_cache->plugins[ $basename ]['is_active'] = $is_site_level_active;
9350 }
9351 }
9352 }
9353
9354 $site_active_plugins_cache->md5 = $site_active_plugins_thumbprint;
9355 $site_active_plugins_cache->timestamp = $time;
9356 self::$_accounts->set_option( $site_active_plugins_option_name, $site_active_plugins_cache, true );
9357
9358 $network_plugins_cache->md5 = $network_plugins_thumbprint;
9359 $network_plugins_cache->timestamp = $time;
9360 self::$_accounts->set_option( $network_plugins_option_name, $network_plugins_cache, true );
9361
9362 return $plugins_update_data;
9363 }
9364
9365 /**
9366 * Return a list of modified themes since the last sync.
9367 *
9368 * Note:
9369 * There's no point to store a themes counter since even if the number of
9370 * themes didn't change, we still need to check if the versions are all the
9371 * same and the activity state is similar.
9372 *
9373 * @author Vova Feldman (@svovaf)
9374 * @since 1.1.8
9375 *
9376 * @return array|false
9377 */
9378 private function get_themes_data_for_api() {
9379 // Alias.
9380 $option_name = 'all_themes';
9381
9382 $all_cached_themes = self::$_accounts->get_option( $option_name );
9383
9384 if ( ! is_object( $all_cached_themes ) ) {
9385 $all_cached_themes = (object) array(
9386 'timestamp' => '',
9387 'md5' => '',
9388 'themes' => array(),
9389 );
9390 }
9391
9392 $time = time();
9393
9394 if ( ! empty( $all_cached_themes->timestamp ) &&
9395 ( $time - $all_cached_themes->timestamp ) < WP_FS__TIME_5_MIN_IN_SEC
9396 ) {
9397 // Don't send theme updates if last update was in the past 5 min.
9398 return false;
9399 }
9400
9401 // Write timestamp to lock the logic.
9402 $all_cached_themes->timestamp = $time;
9403 self::$_accounts->set_option( $option_name, $all_cached_themes, true );
9404
9405 // Reload options from DB.
9406 self::$_accounts->load( true );
9407 $all_cached_themes = self::$_accounts->get_option( $option_name );
9408
9409 if ( $time != $all_cached_themes->timestamp ) {
9410 // If timestamp is different, then another thread captured the lock.
9411 return false;
9412 }
9413
9414 // Get active theme.
9415 $active_theme = wp_get_theme();
9416 $active_theme_stylesheet = $active_theme->get_stylesheet();
9417
9418 // Check if there's a change in themes.
9419 $all_themes = wp_get_themes();
9420
9421 // Check if themes changed.
9422 ksort( $all_themes );
9423
9424 $themes_signature = '';
9425 foreach ( $all_themes as $slug => $data ) {
9426 $is_active = ( $slug === $active_theme_stylesheet );
9427 $themes_signature .= $slug . ',' .
9428 $data->version . ',' .
9429 ( $is_active ? '1' : '0' ) . ';';
9430 }
9431
9432 // Check if themes status changed (version or active/inactive).
9433 $themes_changed = ( $all_cached_themes->md5 !== md5( $themes_signature ) );
9434
9435 $themes_update_data = array();
9436
9437 if ( $themes_changed ) {
9438 // Change in themes, report changes.
9439
9440 // Update existing themes info.
9441 foreach ( $all_cached_themes->themes as $slug => $data ) {
9442 $is_active = ( $slug === $active_theme_stylesheet );
9443
9444 if ( ! isset( $all_themes[ $slug ] ) ) {
9445 // Plugin uninstalled.
9446 $uninstalled_theme_data = $data;
9447 $uninstalled_theme_data['is_active'] = false;
9448 $uninstalled_theme_data['is_uninstalled'] = true;
9449 $themes_update_data[] = $uninstalled_theme_data;
9450
9451 unset( $all_themes[ $slug ] );
9452 unset( $all_cached_themes->themes[ $slug ] );
9453 } else if ( $data['is_active'] !== $is_active ||
9454 $data['version'] !== $all_themes[ $slug ]->version
9455 ) {
9456 // Plugin activated or deactivated, or version changed.
9457
9458 $all_cached_themes->themes[ $slug ]['is_active'] = $is_active;
9459 $all_cached_themes->themes[ $slug ]['version'] = $all_themes[ $slug ]->version;
9460
9461 $themes_update_data[] = $all_cached_themes->themes[ $slug ];
9462 }
9463 }
9464
9465 // Find new themes that weren't yet seen before.
9466 foreach ( $all_themes as $slug => $data ) {
9467 if ( ! isset( $all_cached_themes->themes[ $slug ] ) ) {
9468 $is_active = ( $slug === $active_theme_stylesheet );
9469
9470 // New plugin.
9471 $new_plugin = array(
9472 'slug' => $slug,
9473 'version' => $data->version,
9474 'title' => $data->name,
9475 'is_active' => $is_active,
9476 'is_uninstalled' => false,
9477 );
9478
9479 $themes_update_data[] = $new_plugin;
9480 $all_cached_themes->themes[ $slug ] = $new_plugin;
9481 }
9482 }
9483
9484 $all_cached_themes->md5 = md5( $themes_signature );
9485 $all_cached_themes->timestamp = time();
9486 self::$_accounts->set_option( $option_name, $all_cached_themes, true );
9487 }
9488
9489 return $themes_update_data;
9490 }
9491
9492 /**
9493 * Get site data for API install request.
9494 *
9495 * @author Vova Feldman (@svovaf)
9496 * @since 1.1.2
9497 *
9498 * @param string[] $override
9499 * @param bool $include_plugins Since 1.1.8 by default include plugin changes.
9500 * @param bool $include_themes Since 1.1.8 by default include plugin changes.
9501 * @param bool $include_blog_data Since 2.3.0 by default include the current blog's data (language, title, and URL).
9502 *
9503 * @return array
9504 */
9505 private function get_install_data_for_api(
9506 array $override,
9507 $include_plugins = true,
9508 $include_themes = true,
9509 $include_blog_data = true
9510 ) {
9511 // Alias.
9512 $permissions = FS_Permission_Manager::instance( $this );
9513
9514 if ( $permissions->is_extensions_tracking_allowed() ) {
9515 if ( ! defined( 'WP_FS__TRACK_PLUGINS' ) || false !== WP_FS__TRACK_PLUGINS ) {
9516 /**
9517 * @since 1.1.8 Also send plugin updates.
9518 */
9519 if ( $include_plugins && ! isset( $override['plugins'] ) ) {
9520 $plugins = $this->get_plugins_data_for_api();
9521 if ( ! empty( $plugins ) ) {
9522 $override['plugins'] = $plugins;
9523 }
9524 }
9525 }
9526
9527 if ( ! defined( 'WP_FS__TRACK_THEMES' ) || false !== WP_FS__TRACK_THEMES ) {
9528 /**
9529 * @since 1.1.8 Also send themes updates.
9530 */
9531 if ( $include_themes && ! isset( $override['themes'] ) ) {
9532 $themes = $this->get_themes_data_for_api();
9533 if ( ! empty( $themes ) ) {
9534 $override['themes'] = $themes;
9535 }
9536 }
9537 }
9538 }
9539
9540 $versions = $this->get_versions();
9541
9542 $blog_data = array();
9543 if ( $include_blog_data ) {
9544 $blog_data['url'] = self::get_unfiltered_site_url();
9545
9546 if ( $permissions->is_diagnostic_tracking_allowed() ) {
9547 $blog_data = array_merge( $blog_data, array(
9548 'language' => self::get_sanitized_language(),
9549 'title' => get_bloginfo( 'name' ),
9550 ) );
9551 }
9552 }
9553
9554 return array_merge( $versions, $blog_data, array(
9555 'version' => $this->get_plugin_version(),
9556 'is_premium' => $this->is_premium(),
9557 // Special params.
9558 'is_active' => true,
9559 'is_uninstalled' => false,
9560 ), $override );
9561 }
9562
9563 /**
9564 * Update installs details.
9565 *
9566 * @todo V1 of multiste network support doesn't support plugin and theme data sending.
9567 *
9568 * @author Vova Feldman (@svovaf)
9569 * @since 2.0.0
9570 *
9571 * @param string[] string $override
9572 * @param bool $only_diff
9573 * @param bool $is_keepalive
9574 * @param bool $include_plugins Since 1.1.8 by default include plugin changes.
9575 * @param bool $include_themes Since 1.1.8 by default include plugin changes.
9576 *
9577 * @return array
9578 */
9579 private function get_installs_data_for_api(
9580 array $override,
9581 $only_diff = false,
9582 $is_keepalive = false,
9583 $include_plugins = true,
9584 $include_themes = true
9585 ) {
9586 /**
9587 * @since 1.1.8 Also send plugin updates.
9588 */
9589 // if ( $include_plugins && ! isset( $override['plugins'] ) ) {
9590 // $plugins = $this->get_plugins_data_for_api();
9591 // if ( ! empty( $plugins ) ) {
9592 // $override['plugins'] = $plugins;
9593 // }
9594 // }
9595 /**
9596 * @since 1.1.8 Also send themes updates.
9597 */
9598 // if ( $include_themes && ! isset( $override['themes'] ) ) {
9599 // $themes = $this->get_themes_data_for_api();
9600 // if ( ! empty( $themes ) ) {
9601 // $override['themes'] = $themes;
9602 // }
9603 // }
9604
9605 // Common properties.
9606 $versions = $this->get_versions();
9607 $common = array_merge( $versions, array(
9608 'version' => $this->get_plugin_version(),
9609 'is_premium' => $this->is_premium(),
9610 ), $override );
9611
9612
9613 $is_common_diff_for_any_site = false;
9614 $common_diff_union = array();
9615
9616 $installs_data = array();
9617
9618 $sites = self::get_sites();
9619
9620 $subsite_data_for_api_by_install_id = array();
9621 $install_url_by_install_id = array();
9622 $subsite_registration_date_by_install_id = array();
9623
9624 foreach ( $sites as $site ) {
9625 $blog_id = self::get_site_blog_id( $site );
9626
9627 $install = $this->get_install_by_blog_id( $blog_id );
9628
9629 if ( is_object( $install ) ) {
9630 if ( $install->user_id != $this->_user->id ) {
9631 // Install belongs to a different owner.
9632 continue;
9633 }
9634
9635 if ( ! $this->is_tracking_allowed( $blog_id, $install ) ) {
9636 // Don't send updates regarding opted-out installs.
9637 continue;
9638 }
9639
9640 $install_data = $this->get_site_info( $site, true );
9641
9642 if ( FS_Clone_Manager::instance()->is_temporary_duplicate_by_blog_id( $install_data['blog_id'] ) ) {
9643 continue;
9644 }
9645
9646 $uid = $install_data['uid'];
9647 $url = $install_data['url'];
9648 $registration_date = $install_data['registration_date'];
9649
9650 if ( isset( $subsite_data_for_api_by_install_id[ $install->id ] ) ) {
9651 $clone_subsite_data = $subsite_data_for_api_by_install_id[ $install->id ];
9652 $clone_install_url = $install_url_by_install_id[ $install->id ];
9653 $clone_subsite_registration_date = $subsite_registration_date_by_install_id[ $install->id ];
9654
9655 $skip = false;
9656
9657 if (
9658 ! empty( $install_data['registration_date'] ) &&
9659 ! empty( $clone_subsite_registration_date )
9660 ) {
9661 /**
9662 * 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.
9663 *
9664 * @author Leo Fajardo (@leorw)
9665 * @since 2.5.1
9666 */
9667 $skip = ( strtotime( $install_data['registration_date'] ) > strtotime( $clone_subsite_registration_date ) );
9668 } else if (
9669 /**
9670 * 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.
9671 *
9672 * @author Leo Fajardo (@leorw)
9673 * @since 2.5.0
9674 */
9675 fs_strip_url_protocol( untrailingslashit( $clone_install_url ) ) === fs_strip_url_protocol( untrailingslashit( $clone_subsite_data['url'] ) ) ||
9676 fs_strip_url_protocol( untrailingslashit( $install->url ) ) !== fs_strip_url_protocol( untrailingslashit( $url ) )
9677 ) {
9678 $skip = true;
9679 }
9680
9681 if ( $skip ) {
9682 // 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.
9683 FS_Clone_Manager::instance()->store_blog_install_info( $blog_id );
9684 continue;
9685 }
9686 }
9687
9688 unset( $install_data['blog_id'] );
9689 unset( $install_data['uid'] );
9690 unset( $install_data['url'] );
9691 unset( $install_data['registration_date'] );
9692
9693 $install_data['is_active'] = $this->is_active_for_site( $blog_id );
9694 $install_data['is_uninstalled'] = $install->is_uninstalled;
9695
9696 $common_diff = null;
9697 $is_common_diff = false;
9698 if ( $only_diff ) {
9699 $install_data = $this->get_install_diff_for_api( $install_data, $install, $override );
9700 $common_diff = $this->get_install_diff_for_api( $common, $install, $override );
9701
9702 $is_common_diff = ! empty( $common_diff );
9703
9704 if ( $is_common_diff ) {
9705 foreach ( $common_diff as $k => $v ) {
9706 if ( ! isset( $common_diff_union[ $k ] ) ) {
9707 $common_diff_union[ $k ] = $v;
9708 }
9709 }
9710 }
9711
9712 $is_common_diff_for_any_site = $is_common_diff_for_any_site || $is_common_diff;
9713 }
9714
9715 if ( ! empty( $install_data ) || $is_common_diff || $is_keepalive ) {
9716 // Add install ID and site unique ID.
9717 $install_data['id'] = $install->id;
9718 $install_data['uid'] = $uid;
9719 $install_data['url'] = $url;
9720
9721 $subsite_data_for_api_by_install_id[ $install->id ] = $install_data;
9722 $install_url_by_install_id[ $install->id ] = $install->url;
9723 $subsite_registration_date_by_install_id[ $install->id ] = $registration_date;
9724 }
9725 }
9726 }
9727
9728 restore_current_blog();
9729
9730 $installs_data = array_merge(
9731 $installs_data,
9732 array_values( $subsite_data_for_api_by_install_id )
9733 );
9734
9735 if ( 0 < count( $installs_data ) && ( $is_common_diff_for_any_site || ! $only_diff ) ) {
9736 if ( ! $only_diff ) {
9737 $installs_data[] = $common;
9738 } else if ( ! empty( $common_diff_union ) ) {
9739 $installs_data[] = $common_diff_union;
9740 }
9741 }
9742
9743 foreach ( $installs_data as &$data ) {
9744 $data = (object) $data;
9745 }
9746
9747 return $installs_data;
9748 }
9749
9750 /**
9751 * Compare site actual data to the stored install data and return the differences for an API data sync.
9752 *
9753 * @author Vova Feldman (@svovaf)
9754 * @since 2.0.0
9755 *
9756 * @param array $site
9757 * @param FS_Site $install
9758 * @param string[] string $override
9759 *
9760 * @return array
9761 */
9762 private function get_install_diff_for_api( $site, $install, $override = array() ) {
9763 $diff = array();
9764 $special = array();
9765 $special_override = false;
9766
9767 foreach ( $site as $p => $v ) {
9768 if ( property_exists( $install, $p ) ) {
9769 if ( ( is_bool( $install->{$p} ) || ! empty( $install->{$p} ) ) &&
9770 $install->{$p} != $v
9771 ) {
9772 $val = self::get_api_sanitized_property( $p, $v );
9773
9774 if ( $install->{$p} != $val ) {
9775 $install->{$p} = $val;
9776 $diff[ $p ] = $val;
9777 }
9778 }
9779 } else {
9780 $special[ $p ] = $v;
9781
9782 if ( isset( $override[ $p ] ) ||
9783 'plugins' === $p ||
9784 'themes' === $p
9785 ) {
9786 $special_override = true;
9787 }
9788 }
9789 }
9790
9791 if ( $special_override || 0 < count( $diff ) ) {
9792 // Add special params only if has at least one
9793 // standard param, or if explicitly requested to
9794 // override a special param or a param which is not exist
9795 // in the install object.
9796 $diff = array_merge( $diff, $special );
9797 }
9798
9799 return $diff;
9800 }
9801
9802 /**
9803 * @author Leo Fajardo (@leorw)
9804 * @since 2.5.1
9805 */
9806 private function send_pending_clone_update_once() {
9807 $this->_logger->entrance();
9808
9809 if ( ! empty( $this->_storage->clone_id ) ) {
9810 return;
9811 }
9812
9813 $install_clone = $this->get_api_site_scope()->call(
9814 '/clones',
9815 'post',
9816 array( 'site_url' => self::get_unfiltered_site_url() )
9817 );
9818
9819 if ( $this->is_api_result_entity( $install_clone ) ) {
9820 $this->_storage->clone_id = $install_clone->id;
9821 }
9822 }
9823
9824 /**
9825 * @author Leo Fajardo (@leorw)
9826 * @since 2.5.1
9827 *
9828 * @param string $resolution_type
9829 * @param FS_Site $clone_context_install
9830 */
9831 function send_clone_resolution_update( $resolution_type, $clone_context_install ) {
9832 $this->_logger->entrance();
9833
9834 if ( empty( $this->_storage->clone_id ) ) {
9835 return;
9836 }
9837
9838 $new_install_id = null;
9839 $current_site = null;
9840
9841 $flush = false;
9842
9843 /**
9844 * 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.
9845 */
9846 if ( $clone_context_install->id != $this->_site->id ) {
9847 $new_install_id = $this->_site->id;
9848 $current_site = $this->_site;
9849 $this->_site = $clone_context_install;
9850
9851 $flush = true;
9852 }
9853
9854 $this->get_api_site_scope( $flush )->call(
9855 "/clones/{$this->_storage->clone_id}",
9856 'put',
9857 array(
9858 'resolution' => $resolution_type,
9859 'new_install_id' => $new_install_id,
9860 )
9861 );
9862
9863 if ( is_object( $current_site ) ) {
9864 /**
9865 * Ensure that the install scope entity is updated back to the previous install entity.
9866 */
9867 $this->_site = $current_site;
9868
9869 // Restore the previous install scope entity of the API.
9870 $this->get_api_site_scope( true );
9871 }
9872 }
9873
9874 /**
9875 * Update install only if changed.
9876 *
9877 * @author Vova Feldman (@svovaf)
9878 * @since 1.0.9
9879 *
9880 * @param string[] string $override
9881 * @param bool $flush
9882 * @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.
9883 *
9884 * @return false|object|string
9885 */
9886 private function send_install_update( $override = array(), $flush = false, $is_two_way_sync = false ) {
9887 $this->_logger->entrance();
9888
9889 $check_properties = $this->get_install_data_for_api( $override );
9890
9891 if ( $flush ) {
9892 $params = $check_properties;
9893 } else {
9894 $params = $this->get_install_diff_for_api( $check_properties, $this->_site, $override );
9895 }
9896
9897 if ( empty( $params ) ) {
9898 $keepalive_only_update = $this->should_send_keepalive_update();
9899
9900 if ( ! $keepalive_only_update ) {
9901 /**
9902 * There are no updates to send including keepalive.
9903 *
9904 * @author Leo Fajardo (@leorw)
9905 * @since 2.2.3
9906 */
9907 return false;
9908 }
9909 }
9910
9911 if ( $is_two_way_sync ) {
9912 /**
9913 * Update last install sync timestamp during a two-way sync call as we expect that updates are sent during this call.
9914 *
9915 * @author Leo Fajardo (@leorw)
9916 * @since 2.2.3
9917 */
9918 if ( ! is_multisite() ) {
9919 // Update last install sync timestamp.
9920 $this->set_cron_execution_timestamp( 'install_sync' );
9921 }
9922
9923 $params['uid'] = $this->get_anonymous_id();
9924 }
9925
9926 $this->set_keepalive_timestamp();
9927
9928 // Send updated values to FS.
9929 $site = $this->api_site_call( '/', 'put', $params, true );
9930
9931 if ( $is_two_way_sync && $this->is_api_result_entity( $site ) ) {
9932 /**
9933 * Clear scheduled install sync after a two-way sync call.
9934 *
9935 * @author Leo Fajardo (@leorw)
9936 * @since 2.2.3
9937 */
9938 if ( ! is_multisite() ) {
9939 // I successfully sent install update, clear scheduled sync if exist.
9940 $this->clear_install_sync_cron();
9941 }
9942 }
9943
9944 return $site;
9945 }
9946
9947 /**
9948 * Update installs only if changed.
9949 *
9950 * @author Vova Feldman (@svovaf)
9951 * @since 2.0.0
9952 *
9953 * @param string[] string $override
9954 * @param bool $flush
9955 * @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.
9956 *
9957 * @return false|object|string
9958 */
9959 private function send_installs_update( $override = array(), $flush = false, $is_two_way_sync = false ) {
9960 $this->_logger->entrance();
9961
9962 /**
9963 * Pass `true` to use the network level storage since the update is for many installs.
9964 *
9965 * @author Leo Fajardo (@leorw)
9966 * @since 2.2.3
9967 */
9968 $should_send_keepalive = $this->should_send_keepalive_update( true );
9969
9970 $installs_data = $this->get_installs_data_for_api( $override, ! $flush, $should_send_keepalive );
9971
9972 if ( empty( $installs_data ) ) {
9973 return false;
9974 }
9975
9976 if ( $is_two_way_sync ) {
9977 // Update last install sync timestamp during a two-way sync call as we expect that updates are sent during this call.
9978 $this->set_cron_execution_timestamp( 'install_sync' );
9979 }
9980
9981 /**
9982 * Pass `true` to use the network level storage since the update is for many installs.
9983 *
9984 * @author Leo Fajardo (@leorw)
9985 * @since 2.2.3
9986 */
9987 $this->set_keepalive_timestamp( true );
9988
9989 // Send updated values to FS.
9990 $result = $this->get_api_user_scope()->call( "/plugins/{$this->_plugin->id}/installs.json", 'put', $installs_data );
9991
9992 if ( $is_two_way_sync && $this->is_api_result_object( $result, 'installs' ) ) {
9993 // I successfully sent a two-way installs update, clear the scheduled install sync if it exists.
9994 $this->clear_install_sync_cron();
9995 }
9996
9997 return $result;
9998 }
9999
10000 /**
10001 * @author Leo Fajardo (@leorw)
10002 *
10003 * @param bool|null $use_network_level_storage
10004 *
10005 * @return bool
10006 */
10007 private function should_send_keepalive_update( $use_network_level_storage = null ) {
10008 $keepalive_timestamp = $this->_storage->get( 'keepalive_timestamp', 0, $use_network_level_storage );
10009
10010 if ( $keepalive_timestamp < ( time() - WP_FS__TIME_WEEK_IN_SEC ) ) {
10011 // If updated more than 7 days ago, trigger a keepalive and update the time it was triggered.
10012 return true;
10013 } else {
10014 // 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.
10015 return ( 7 == rand( 1, 7 ) );
10016 }
10017 }
10018
10019 /**
10020 * Syncs the install owner's data if needed (i.e., if the install owner is different from the loaded user).
10021 *
10022 * @author Leo Fajardo (@leorw)
10023 * @since 2.3.2
10024 */
10025 private function maybe_sync_install_user() {
10026 if ( $this->_user->id == $this->_site->user_id ) {
10027 return;
10028 }
10029
10030 // Fetch user data and store if found.
10031 $this->sync_user_by_current_install();
10032 }
10033
10034 /**
10035 * Update install only if changed.
10036 *
10037 * @author Vova Feldman (@svovaf)
10038 * @since 1.0.9
10039 *
10040 * @param string[] string $override
10041 * @param bool $flush
10042 */
10043 function sync_install( $override = array(), $flush = false ) {
10044 $this->_logger->entrance();
10045
10046 $site = $this->send_install_update( $override, $flush, true );
10047
10048 if ( false === $site ) {
10049 // No sync required.
10050 return;
10051 }
10052
10053 if ( ! $this->is_api_result_entity( $site ) ) {
10054 // Failed to sync, don't update locally.
10055 return;
10056 }
10057
10058 $this->_site = new FS_Site( $site );
10059
10060 $this->_store_site( true );
10061 }
10062
10063 /**
10064 * Update install only if changed.
10065 *
10066 * @author Vova Feldman (@svovaf)
10067 * @since 1.0.9
10068 *
10069 * @param string[] string $override
10070 * @param bool $flush
10071 */
10072 private function sync_installs( $override = array(), $flush = false ) {
10073 $this->_logger->entrance();
10074
10075 $result = $this->send_installs_update( $override, $flush, true );
10076
10077 if ( false === $result ) {
10078 // No sync required.
10079 return;
10080 }
10081
10082 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
10083 // Failed to sync, don't update locally.
10084 return;
10085 }
10086
10087 $address_to_blog_map = $this->get_address_to_blog_map();
10088
10089 foreach ( $result->installs as $install ) {
10090 $this->_site = new FS_Site( $install );
10091
10092 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
10093 $blog_id = $address_to_blog_map[ $address ];
10094
10095 $this->_store_site( true, $blog_id );
10096 }
10097 }
10098
10099 /**
10100 * Track install's custom event.
10101 *
10102 * IMPORTANT:
10103 * Custom event tracking is currently only supported for specific clients.
10104 * If you are not one of them, please don't use this method. If you will,
10105 * the API will simply ignore your request based on the plugin ID.
10106 *
10107 * Need custom tracking for your plugin or theme?
10108 * If you are interested in custom event tracking please contact yo@freemius.com
10109 * for further details.
10110 *
10111 * @author Vova Feldman (@svovaf)
10112 * @since 1.2.1
10113 *
10114 * @param string $name Event name.
10115 * @param array $properties Associative key/value array with primitive values only
10116 * @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
10117 * @param bool $once If true, event will be tracked only once. IMPORTANT: Still trigger the API call.
10118 *
10119 * @return object|false Event data or FALSE on failure.
10120 *
10121 * @throws \Freemius_InvalidArgumentException
10122 */
10123 public function track_event( $name, $properties = array(), $process_at = false, $once = false ) {
10124 $this->_logger->entrance( http_build_query( array( 'name' => $name, 'once' => $once ) ) );
10125
10126 if ( ! $this->is_registered() ) {
10127 return false;
10128 }
10129
10130 $event = array( 'type' => $name );
10131
10132 if ( is_numeric( $process_at ) && $process_at > time() ) {
10133 $event['process_at'] = $process_at;
10134 }
10135
10136 if ( $once ) {
10137 $event['once'] = true;
10138 }
10139
10140 if ( ! empty( $properties ) ) {
10141 // Verify associative array values are primitive.
10142 foreach ( $properties as $k => $v ) {
10143 if ( ! is_scalar( $v ) ) {
10144 throw new Freemius_InvalidArgumentException( 'The $properties argument must be an associative key/value array with primitive values only.' );
10145 }
10146 }
10147
10148 $event['properties'] = $properties;
10149 }
10150
10151 $result = $this->get_api_site_scope()->call( 'events.json', 'post', $event );
10152
10153 return $this->is_api_error( $result ) ?
10154 false :
10155 $result;
10156 }
10157
10158 /**
10159 * Track install's custom event only once, but it still triggers the API call.
10160 *
10161 * IMPORTANT:
10162 * Custom event tracking is currently only supported for specific clients.
10163 * If you are not one of them, please don't use this method. If you will,
10164 * the API will simply ignore your request based on the plugin ID.
10165 *
10166 * Need custom tracking for your plugin or theme?
10167 * If you are interested in custom event tracking please contact yo@freemius.com
10168 * for further details.
10169 *
10170 * @author Vova Feldman (@svovaf)
10171 * @since 1.2.1
10172 *
10173 * @param string $name Event name.
10174 * @param array $properties Associative key/value array with primitive values only
10175 * @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
10176 *
10177 * @return object|false Event data or FALSE on failure.
10178 *
10179 * @throws \Freemius_InvalidArgumentException
10180 *
10181 * @user Freemius::track_event()
10182 */
10183 public function track_event_once( $name, $properties = array(), $process_at = false ) {
10184 return $this->track_event( $name, $properties, $process_at, true );
10185 }
10186
10187 /**
10188 * Plugin uninstall hook.
10189 *
10190 * @author Vova Feldman (@svovaf)
10191 * @since 1.0.1
10192 *
10193 * @param bool $check_user Enforce checking if user have plugins activation privileges.
10194 */
10195 function _uninstall_plugin_event( $check_user = true ) {
10196 $this->_logger->entrance( 'slug = ' . $this->_slug );
10197
10198 if ( $check_user && ! current_user_can( 'activate_plugins' ) ) {
10199 return;
10200 }
10201
10202 $params = array();
10203 $uninstall_reason = null;
10204 if ( isset( $this->_storage->uninstall_reason ) ) {
10205 $uninstall_reason = $this->_storage->uninstall_reason;
10206 $params['reason_id'] = $uninstall_reason->id;
10207 $params['reason_info'] = $uninstall_reason->info;
10208 }
10209
10210 if ( ! $this->is_registered() ) {
10211 // Send anonymous uninstall event only if user submitted a feedback.
10212 if ( isset( $uninstall_reason ) ) {
10213 if ( isset( $uninstall_reason->is_anonymous ) && ! $uninstall_reason->is_anonymous ) {
10214 $this->opt_in( false, false, false, false, true );
10215 } else {
10216 $params['uid'] = $this->get_anonymous_id();
10217 $this->get_api_plugin_scope()->call( 'uninstall.json', 'put', $params );
10218 }
10219 }
10220 } else {
10221 $params = array_merge( $params, array(
10222 'is_active' => false,
10223 'is_uninstalled' => true,
10224 ) );
10225
10226 if ( $this->_is_network_active ) {
10227 // Send uninstall event.
10228 $this->send_installs_update( $params );
10229 } else {
10230 // Send uninstall event and handle the result.
10231 $this->sync_install( $params );
10232 }
10233 }
10234
10235 // @todo Decide if we want to delete plugin information from db.
10236 }
10237
10238 /**
10239 * Set the basename of the current product and hook _activate_plugin_event_hook() to the activation action.
10240 *
10241 * @author Vova Feldman (@svovaf)
10242 * @since 2.2.1
10243 *
10244 * @param string $is_premium
10245 * @param string $caller
10246 *
10247 * @return string
10248 */
10249 function set_basename( $is_premium, $caller ) {
10250 $basename = plugin_basename( $caller );
10251
10252 $current_basename = $is_premium ?
10253 $this->_premium_plugin_basename :
10254 $this->_free_plugin_basename;
10255
10256 if ( $current_basename == $basename ) {
10257 // Basename value set correctly.
10258 return;
10259 }
10260
10261 if ( $is_premium ) {
10262 $this->_premium_plugin_basename = $basename;
10263 } else {
10264 $this->_free_plugin_basename = $basename;
10265 }
10266
10267 $plugin_dir = dirname( $this->_plugin_dir_path ) . '/';
10268
10269 register_activation_hook(
10270 $plugin_dir . $basename,
10271 array( &$this, '_activate_plugin_event_hook' )
10272 );
10273 }
10274
10275 /**
10276 * @author Vova Feldman (@svovaf)
10277 * @since 1.1.1
10278 * @since 2.2.1 If the context product is in its premium version, use the current module's basename, even if it was renamed.
10279 *
10280 * @return string
10281 */
10282 function premium_plugin_basename() {
10283 if ( ! isset( $this->_premium_plugin_basename ) ) {
10284 $this->_premium_plugin_basename = $this->is_premium() ?
10285 // The product is premium, so use the current basename.
10286 $this->_plugin_basename :
10287 $this->get_premium_slug() . '/' . basename( $this->_free_plugin_basename );
10288 }
10289
10290 return $this->_premium_plugin_basename;
10291 }
10292
10293 /**
10294 * Uninstall plugin hook. Called only when connected his account with Freemius for active sites tracking.
10295 *
10296 * @author Vova Feldman (@svovaf)
10297 * @since 1.0.2
10298 */
10299 public static function _uninstall_plugin_hook() {
10300 self::_load_required_static();
10301
10302 self::$_static_logger->entrance();
10303
10304 if ( ! current_user_can( 'activate_plugins' ) ) {
10305 return;
10306 }
10307
10308 $plugin_file = substr( current_filter(), strlen( 'uninstall_' ) );
10309
10310 self::$_static_logger->info( 'plugin = ' . $plugin_file );
10311
10312 define( 'WP_FS__UNINSTALL_MODE', true );
10313
10314 $fs = self::get_instance_by_file( $plugin_file );
10315
10316 if ( is_object( $fs ) ) {
10317 $fs->remove_sdk_reference();
10318
10319 self::require_plugin_essentials();
10320
10321 if ( is_plugin_active( $fs->_free_plugin_basename ) ||
10322 is_plugin_active( $fs->premium_plugin_basename() )
10323 ) {
10324 // Deleting Free or Premium plugin version while the other version still installed.
10325 return;
10326 }
10327
10328 if (
10329 ! $fs->is_clone() &&
10330 /**
10331 * 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).
10332 *
10333 * @author Leo Fajardo
10334 */
10335 ( ! is_object( $fs->_site ) || $fs->is_registered() )
10336 ) {
10337 $fs->_uninstall_plugin_event();
10338 }
10339
10340 $fs->do_action( 'after_uninstall' );
10341 }
10342 }
10343
10344 #----------------------------------------------------------------------------------
10345 #region Plugin Information
10346 #----------------------------------------------------------------------------------
10347
10348 /**
10349 * Load WordPress core plugin.php essential module.
10350 *
10351 * @author Vova Feldman (@svovaf)
10352 * @since 1.1.1
10353 */
10354 private static function require_plugin_essentials() {
10355 if ( ! function_exists( 'get_plugins' ) ) {
10356 self::$_static_logger->log( 'Including wp-admin/includes/plugin.php...' );
10357
10358 require_once ABSPATH . 'wp-admin/includes/plugin.php';
10359 }
10360 }
10361
10362 /**
10363 * Load WordPress core pluggable.php module.
10364 *
10365 * @author Vova Feldman (@svovaf)
10366 * @since 1.1.2
10367 */
10368 private static function require_pluggable_essentials() {
10369 if ( ! function_exists( 'wp_get_current_user' ) ) {
10370 require_once ABSPATH . 'wp-includes/pluggable.php';
10371 }
10372 }
10373
10374 /**
10375 * Return plugin data.
10376 *
10377 * @author Vova Feldman (@svovaf)
10378 * @since 1.0.1
10379 *
10380 * @param bool $reparse_plugin_metadata
10381 *
10382 * @return array
10383 */
10384 function get_plugin_data( $reparse_plugin_metadata = false ) {
10385 if ( ! isset( $this->_plugin_data ) || $reparse_plugin_metadata ) {
10386 self::require_plugin_essentials();
10387
10388 if ( $this->is_plugin() ) {
10389 /**
10390 * @author Vova Feldman (@svovaf)
10391 * @since 1.2.0 When using get_plugin_data() do NOT translate plugin data.
10392 *
10393 * @link https://github.com/Freemius/wordpress-sdk/issues/77
10394 */
10395 $plugin_data = get_plugin_data(
10396 $this->_plugin_main_file_path,
10397 false,
10398 false
10399 );
10400 } else {
10401 $theme_data = wp_get_theme();
10402
10403 if ( $this->_plugin_basename !== $theme_data->get_stylesheet() && is_child_theme() ) {
10404 $parent_theme = $theme_data->parent();
10405
10406 if ( ( $parent_theme instanceof WP_Theme ) && $this->_plugin_basename === $parent_theme->get_stylesheet() ) {
10407 $theme_data = $parent_theme;
10408 }
10409 }
10410
10411 $plugin_data = array(
10412 'Name' => $theme_data->get( 'Name' ),
10413 'Version' => $theme_data->get( 'Version' ),
10414 'Author' => $theme_data->get( 'Author' ),
10415 'Description' => $theme_data->get( 'Description' ),
10416 'PluginURI' => $theme_data->get( 'ThemeURI' ),
10417 );
10418 }
10419
10420 $this->_plugin_data = $plugin_data;
10421 }
10422
10423 return $this->_plugin_data;
10424 }
10425
10426 /**
10427 * @author Vova Feldman (@svovaf)
10428 * @since 1.0.1
10429 * @since 1.2.2.5 If slug not set load slug by module ID.
10430 *
10431 * @return string Plugin slug.
10432 */
10433 function get_slug() {
10434 if ( ! isset( $this->_slug ) ) {
10435 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
10436 $this->_slug = $id_slug_type_path_map[ $this->_module_id ]['slug'];
10437 }
10438
10439 return $this->_slug;
10440 }
10441
10442 /**
10443 * @author Leo Fajardo (@leorw)
10444 * @since 2.2.1
10445 *
10446 * @return string
10447 */
10448 function get_premium_slug() {
10449 return ( is_object( $this->_plugin ) && ! empty( $this->_plugin->premium_slug ) ) ?
10450 $this->_plugin->premium_slug :
10451 "{$this->_slug}-premium";
10452 }
10453
10454 /**
10455 * Retrieve the desired folder name for the product.
10456 *
10457 * @author Vova Feldman (@svovaf)
10458 * @since 1.2.1.7
10459 *
10460 * @return string Plugin slug.
10461 */
10462 function get_target_folder_name() {
10463 return $this->can_use_premium_code() ?
10464 $this->_plugin->premium_slug :
10465 $this->_slug;
10466 }
10467
10468 /**
10469 * @author Vova Feldman (@svovaf)
10470 * @since 1.0.1
10471 *
10472 * @return number Plugin ID.
10473 */
10474 function get_id() {
10475 return $this->_plugin->id;
10476 }
10477
10478 /**
10479 * @author Leo Fajardo (@leorw)
10480 * @since 2.2.4
10481 *
10482 * @return number|null Bundle ID.
10483 */
10484 function get_bundle_id() {
10485 return ( isset( $this->_plugin->bundle_id ) && FS_Plugin::is_valid_id( $this->_plugin->bundle_id ) ) ?
10486 $this->_plugin->bundle_id :
10487 null;
10488 }
10489
10490 /**
10491 * @author Vova Feldman (@svovaf)
10492 * @since 2.3.1
10493 *
10494 * @return string|null Bundle public key.
10495 */
10496 function get_bundle_public_key() {
10497 return isset( $this->_plugin->bundle_public_key ) ?
10498 $this->_plugin->bundle_public_key :
10499 null;
10500 }
10501
10502 /**
10503 * Get whether the SDK has been initiated in the context of a Bundle.
10504 *
10505 * This will return true, if `bundle_id` is present in the SDK init parameters.
10506 *
10507 * ```php
10508 * $my_fs = fs_dynamic_init( array(
10509 * // ...
10510 * 'bundle_id' => 'XXXX', // Will return true since we have bundle id.
10511 * 'bundle_public_key' => 'pk_XXXX',
10512 * ) );
10513 * ```
10514 *
10515 * @author Swashata Ghosh (@swashata)
10516 * @since 2.5.0
10517 *
10518 * @return bool True if we are running in bundle context, false otherwise.
10519 */
10520 private function has_bundle_context() {
10521 return ! is_null( $this->get_bundle_id() );
10522 }
10523
10524 /**
10525 * @author Vova Feldman (@svovaf)
10526 * @since 1.2.1.5
10527 *
10528 * @return string Freemius SDK version
10529 */
10530 function get_sdk_version() {
10531 return $this->version;
10532 }
10533
10534 /**
10535 * @author Vova Feldman (@svovaf)
10536 * @since 1.2.1.5
10537 *
10538 * @return number Parent plugin ID (if parent exist).
10539 */
10540 function get_parent_id() {
10541 return $this->is_addon() ?
10542 $this->get_parent_instance()->get_id() :
10543 $this->_plugin->id;
10544 }
10545
10546 /**
10547 * @author Vova Feldman (@svovaf)
10548 * @since 2.3.1
10549 *
10550 * @return string
10551 */
10552 function get_usage_tracking_terms_url() {
10553 return $this->apply_filters(
10554 'usage_tracking_terms_url',
10555 "https://freemius.com/wordpress/usage-tracking/{$this->_plugin->id}/{$this->_slug}/"
10556 );
10557 }
10558
10559 /**
10560 * @author Vova Feldman (@svovaf)
10561 * @since 2.3.1
10562 *
10563 * @return string
10564 */
10565 function get_eula_url() {
10566 return $this->apply_filters(
10567 'eula_url',
10568 "https://freemius.com/product/{$this->_plugin->id}/{$this->_slug}/legal/eula/"
10569 );
10570 }
10571
10572 /**
10573 * @author Vova Feldman (@svovaf)
10574 * @since 1.0.1
10575 *
10576 * @return string Plugin public key.
10577 */
10578 function get_public_key() {
10579 return $this->_plugin->public_key;
10580 }
10581
10582 /**
10583 * Will be available only on sandbox mode.
10584 *
10585 * @author Vova Feldman (@svovaf)
10586 * @since 1.0.4
10587 *
10588 * @return mixed Plugin secret key.
10589 */
10590 function get_secret_key() {
10591 return $this->_plugin->secret_key;
10592 }
10593
10594 /**
10595 * @author Vova Feldman (@svovaf)
10596 * @since 1.1.1
10597 *
10598 * @return bool
10599 */
10600 function has_secret_key() {
10601 return ! empty( $this->_plugin->secret_key );
10602 }
10603
10604 /**
10605 * @author Vova Feldman (@svovaf)
10606 * @since 1.0.9
10607 *
10608 * @param string|bool $premium_suffix
10609 *
10610 * @return string
10611 */
10612 function get_plugin_name( $premium_suffix = false ) {
10613 $this->_logger->entrance();
10614
10615 /**
10616 * This `if-else` can be squeezed into a single `if` but I intentionally split it for code readability.
10617 *
10618 * @author Vova Feldman
10619 */
10620 if ( ! isset( $this->_plugin_name ) ) {
10621 // Name is not yet set.
10622 $this->set_name( $premium_suffix );
10623 } else if (
10624 ! empty( $premium_suffix ) &&
10625 ( ! is_object( $this->_plugin ) || $this->_plugin->premium_suffix !== $premium_suffix )
10626 ) {
10627 // Name is already set, but there's a change in the premium suffix.
10628 $this->set_name( $premium_suffix );
10629 }
10630
10631 return $this->_plugin_name;
10632 }
10633
10634 /**
10635 * Calculates and stores the product's name. This helper function was created specifically for get_plugin_name() just to make the code clearer.
10636 *
10637 * @author Vova Feldman (@svovaf)
10638 * @since 2.2.1
10639 *
10640 * @param string $premium_suffix
10641 */
10642 private function set_name( $premium_suffix = '' ) {
10643 $plugin_data = $this->get_plugin_data();
10644
10645 // Get name.
10646 $this->_plugin_name = $plugin_data['Name'];
10647
10648 if ( is_string( $premium_suffix ) ) {
10649 $premium_suffix = trim( $premium_suffix );
10650
10651 if ( ! empty( $premium_suffix ) ) {
10652 // Check if plugin name contains " (premium)" or a custom suffix and remove it.
10653 $suffix = ( ' ' . strtolower( $premium_suffix ) );
10654 $suffix_len = strlen( $suffix );
10655
10656 if ( strlen( $plugin_data['Name'] ) > $suffix_len &&
10657 $suffix === substr( strtolower( $plugin_data['Name'] ), - $suffix_len )
10658 ) {
10659 $this->_plugin_name = substr( $plugin_data['Name'], 0, - $suffix_len );
10660 }
10661 }
10662 }
10663
10664 $this->_logger->departure( 'Name = ' . $this->_plugin_name );
10665 }
10666
10667 /**
10668 * @author Vova Feldman (@svovaf)
10669 * @since 1.0.0
10670 *
10671 * @param bool $reparse_plugin_metadata
10672 *
10673 * @return string
10674 */
10675 function get_plugin_version( $reparse_plugin_metadata = false ) {
10676 $this->_logger->entrance();
10677
10678 $plugin_data = $this->get_plugin_data( $reparse_plugin_metadata );
10679
10680 $this->_logger->departure( 'Version = ' . $plugin_data['Version'] );
10681
10682 return $this->apply_filters( 'plugin_version', $plugin_data['Version'] );
10683 }
10684
10685 /**
10686 * @author Vova Feldman (@svovaf)
10687 * @since 1.2.1.7
10688 *
10689 * @return string
10690 */
10691 function get_plugin_title() {
10692 $this->_logger->entrance();
10693
10694 $title = $this->_plugin->title;
10695
10696 return $this->apply_filters( 'plugin_title', $title );
10697 }
10698
10699 /**
10700 * @author Vova Feldman (@svovaf)
10701 * @since 1.2.2.7
10702 *
10703 * @param bool $lowercase
10704 *
10705 * @return string
10706 */
10707 function get_module_label( $lowercase = false ) {
10708 $label = $this->is_addon() ?
10709 $this->get_text_inline( 'Add-On', 'addon' ) :
10710 ( $this->is_plugin() ?
10711 $this->get_text_inline( 'Plugin', 'plugin' ) :
10712 $this->get_text_inline( 'Theme', 'theme' ) );
10713
10714 if ( $lowercase ) {
10715 $label = strtolower( $label );
10716 }
10717
10718 return $label;
10719 }
10720
10721 /**
10722 * @author Vova Feldman (@svovaf)
10723 * @since 1.0.4
10724 *
10725 * @return string
10726 */
10727 function get_plugin_basename() {
10728 if ( ! isset( $this->_plugin_basename ) ) {
10729 if ( $this->is_plugin() ) {
10730 $this->_plugin_basename = plugin_basename( $this->_plugin_main_file_path );
10731 } else {
10732 $this->_plugin_basename = basename( dirname( $this->_plugin_main_file_path ) );
10733 }
10734 }
10735
10736 return $this->_plugin_basename;
10737 }
10738
10739 function get_plugin_folder_name() {
10740 $this->_logger->entrance();
10741
10742 $plugin_folder = $this->_plugin_basename;
10743
10744 while ( '.' !== dirname( $plugin_folder ) ) {
10745 $plugin_folder = dirname( $plugin_folder );
10746 }
10747
10748 $this->_logger->departure( 'Folder Name = ' . $plugin_folder );
10749
10750 return $plugin_folder;
10751 }
10752
10753 #endregion ------------------------------------------------------------------
10754
10755 /* Account
10756 ------------------------------------------------------------------------------------------------------------------*/
10757
10758 /**
10759 * Find plugin's slug by plugin's basename.
10760 *
10761 * @author Vova Feldman (@svovaf)
10762 * @since 1.0.9
10763 *
10764 * @param string $plugin_base_name
10765 *
10766 * @return false|string
10767 */
10768 private static function find_slug_by_basename( $plugin_base_name ) {
10769 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
10770
10771 if ( ! array( $file_slug_map ) || ! isset( $file_slug_map[ $plugin_base_name ] ) ) {
10772 return false;
10773 }
10774
10775 return $file_slug_map[ $plugin_base_name ];
10776 }
10777
10778 /**
10779 * Store the map between the plugin's basename to the slug.
10780 *
10781 * @author Vova Feldman (@svovaf)
10782 * @since 1.0.9
10783 */
10784 private function store_file_slug_map() {
10785 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
10786
10787 if ( ! array( $file_slug_map ) ) {
10788 $file_slug_map = array();
10789 }
10790
10791 if ( ! isset( $file_slug_map[ $this->_plugin_basename ] ) ||
10792 $file_slug_map[ $this->_plugin_basename ] !== $this->_slug
10793 ) {
10794 $file_slug_map[ $this->_plugin_basename ] = $this->_slug;
10795 self::$_accounts->set_option( 'file_slug_map', $file_slug_map, true );
10796 }
10797 }
10798
10799 /**
10800 * @return array[number]FS_User
10801 */
10802 static function get_all_users() {
10803 $users = self::maybe_get_entities_account_option( 'users', array() );
10804
10805 if ( ! is_array( $users ) ) {
10806 $users = array();
10807 }
10808
10809 return $users;
10810 }
10811
10812 /**
10813 * @param string $module_type
10814 * @param null|int $blog_id Since 2.0.0
10815 *
10816 * @return array[string]FS_Site
10817 */
10818 private static function get_all_sites(
10819 $module_type = WP_FS__MODULE_TYPE_PLUGIN,
10820 $blog_id = null,
10821 $is_backup = false
10822 ) {
10823 $sites = self::get_account_option(
10824 ( $is_backup ? 'prev_' : '' ) . 'sites',
10825 $module_type,
10826 $blog_id
10827 );
10828
10829 if ( ! is_array( $sites ) ) {
10830 $sites = array();
10831 }
10832
10833 return $sites;
10834 }
10835
10836 /**
10837 * @author Leo Fajardo (@leorw)
10838 *
10839 * @since 1.2.2
10840 *
10841 * @param string $option_name
10842 * @param string $module_type
10843 * @param null|int $network_level_or_blog_id Since 2.0.0
10844 *
10845 * @return mixed
10846 */
10847 private static function get_account_option( $option_name, $module_type = null, $network_level_or_blog_id = null ) {
10848 if ( ! is_null( $module_type ) && WP_FS__MODULE_TYPE_PLUGIN !== $module_type ) {
10849 $option_name = $module_type . '_' . $option_name;
10850 }
10851
10852 return self::maybe_get_entities_account_option( $option_name, array(), $network_level_or_blog_id );
10853 }
10854
10855 /**
10856 * @author Leo Fajardo (@leorw)
10857 *
10858 * @since 1.2.2
10859 *
10860 * @param string $option_name
10861 * @param mixed $option_value
10862 * @param bool $store
10863 * @param null|int $network_level_or_blog_id Since 2.0.0
10864 */
10865 private function set_account_option( $option_name, $option_value, $store, $network_level_or_blog_id = null ) {
10866 self::set_account_option_by_module(
10867 $this->_module_type,
10868 $option_name,
10869 $option_value,
10870 $store,
10871 $network_level_or_blog_id
10872 );
10873 }
10874
10875 /**
10876 * @author Vova Feldman (@svovaf)
10877 *
10878 * @since 1.2.2.7
10879 *
10880 * @param string $module_type
10881 * @param string $option_name
10882 * @param mixed $option_value
10883 * @param bool $store
10884 * @param null|int $network_level_or_blog_id Since 2.0.0
10885 */
10886 private static function set_account_option_by_module(
10887 $module_type,
10888 $option_name,
10889 $option_value,
10890 $store,
10891 $network_level_or_blog_id = null
10892 ) {
10893 if ( WP_FS__MODULE_TYPE_PLUGIN != $module_type ) {
10894 $option_name = $module_type . '_' . $option_name;
10895 }
10896
10897 self::$_accounts->set_option( $option_name, $option_value, $store, $network_level_or_blog_id );
10898 }
10899
10900 /**
10901 * This method can also return non-entity or non-entities collection option like the `user_id_license_ids_map` option.
10902 *
10903 * @author Leo Fajardo (@leorw)
10904 * @since 2.3.1
10905 *
10906 * @param string $option_name
10907 * @param mixed $default
10908 * @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).
10909 *
10910 * @return mixed|FS_Plugin[]|FS_User[]|FS_Site[]|FS_Plugin_License[]|FS_Plugin_Plan[]|FS_Plugin_Tag[]
10911 */
10912 private static function maybe_get_entities_account_option( $option_name, $default = null, $network_level_or_blog_id = null ) {
10913 $option = self::$_accounts->get_option( $option_name, $default, $network_level_or_blog_id );
10914
10915 $class_name = '';
10916
10917 if ( fs_starts_with( $option_name, WP_FS__MODULE_TYPE_THEME . '_' ) ) {
10918 $option_name = str_replace( WP_FS__MODULE_TYPE_THEME . '_', '', $option_name );
10919 }
10920
10921 switch ( $option_name ) {
10922 case 'plugins':
10923 case 'themes':
10924 case 'addons':
10925 $class_name = FS_Plugin::get_class_name();
10926 break;
10927 case 'users':
10928 $class_name = FS_User::get_class_name();
10929 break;
10930 case 'sites':
10931 $class_name = FS_Site::get_class_name();
10932 break;
10933 case 'licenses':
10934 case 'all_licenses':
10935 $class_name = FS_Plugin_License::get_class_name();
10936 break;
10937 case 'plans':
10938 $class_name = FS_Plugin_Plan::get_class_name();
10939 break;
10940 case 'updates':
10941 $class_name = FS_Plugin_Tag::get_class_name();
10942 break;
10943 }
10944
10945 if ( empty( $class_name ) ) {
10946 return $option;
10947 }
10948
10949 return fs_get_entities( $option, $class_name );
10950 }
10951
10952 /**
10953 * @author Vova Feldman (@svovaf)
10954 * @since 1.0.6
10955 *
10956 * @param number|null $module_id
10957 *
10958 * @return FS_Plugin_License[]
10959 */
10960 private static function get_all_licenses( $module_id = null ) {
10961 $licenses = self::get_account_option( 'all_licenses' );
10962
10963 if ( ! is_array( $licenses ) ) {
10964 $licenses = array();
10965 }
10966
10967 if ( is_null( $module_id ) ) {
10968 return $licenses;
10969 }
10970
10971 $licenses = isset( $licenses[ $module_id ] ) ?
10972 $licenses[ $module_id ] :
10973 array();
10974
10975 return $licenses;
10976 }
10977
10978 /**
10979 * @author Leo Fajardo (@leorw)
10980 * @since 2.0.0
10981 *
10982 * @return array
10983 */
10984 private static function get_all_licenses_by_module_type() {
10985 $licenses = self::get_account_option( 'all_licenses' );
10986
10987 $licenses_by_module_type = array(
10988 WP_FS__MODULE_TYPE_PLUGIN => array(),
10989 WP_FS__MODULE_TYPE_THEME => array()
10990 );
10991
10992 if ( ! is_array( $licenses ) ) {
10993 return $licenses_by_module_type;
10994 }
10995
10996 foreach ( $licenses as $module_id => $module_licenses ) {
10997 $fs = self::get_instance_by_id( $module_id );
10998 if ( false === $fs ) {
10999 continue;
11000 }
11001
11002 $licenses_by_module_type[ $fs->_module_type ] = array_merge( $licenses_by_module_type[ $fs->_module_type ], $module_licenses );
11003 }
11004
11005 return $licenses_by_module_type;
11006 }
11007
11008 /**
11009 * @author Leo Fajardo (@leorw)
11010 * @since 2.0.0
11011 *
11012 * @param number $module_id
11013 * @param number|null $user_id
11014 *
11015 * @return array
11016 */
11017 private static function get_user_id_license_ids_map( $module_id, $user_id = null ) {
11018 $all_modules_user_id_license_ids_map = self::get_account_option( 'user_id_license_ids_map' );
11019
11020 if ( ! is_array( $all_modules_user_id_license_ids_map ) ) {
11021 $all_modules_user_id_license_ids_map = array();
11022 }
11023
11024 $user_id_license_ids_map = isset( $all_modules_user_id_license_ids_map[ $module_id ] ) ?
11025 $all_modules_user_id_license_ids_map[ $module_id ] :
11026 array();
11027
11028 if ( FS_User::is_valid_id( $user_id ) ) {
11029 $user_id_license_ids_map = isset( $user_id_license_ids_map[ $user_id ] ) ?
11030 $user_id_license_ids_map[ $user_id ] :
11031 array();
11032 }
11033
11034 return $user_id_license_ids_map;
11035 }
11036
11037 /**
11038 * @author Leo Fajardo (@leorw)
11039 * @since 2.0.0
11040 *
11041 * @param array $new_user_id_license_ids_map
11042 * @param number $module_id
11043 * @param number|null $user_id
11044 */
11045 private static function store_user_id_license_ids_map( $new_user_id_license_ids_map, $module_id, $user_id = null ) {
11046 $all_modules_user_id_license_ids_map = self::get_account_option( 'user_id_license_ids_map' );
11047 if ( ! is_array( $all_modules_user_id_license_ids_map ) ) {
11048 $all_modules_user_id_license_ids_map = array();
11049 }
11050
11051 if ( ! isset( $all_modules_user_id_license_ids_map[ $module_id ] ) ) {
11052 $all_modules_user_id_license_ids_map[ $module_id ] = array();
11053 }
11054
11055 if ( FS_User::is_valid_id( $user_id ) ) {
11056 $all_modules_user_id_license_ids_map[ $module_id ][ $user_id ] = $new_user_id_license_ids_map;
11057 } else {
11058 $all_modules_user_id_license_ids_map[ $module_id ] = $new_user_id_license_ids_map;
11059 }
11060
11061 self::$_accounts->set_option( 'user_id_license_ids_map', $all_modules_user_id_license_ids_map, true );
11062 }
11063
11064 /**
11065 * Get a collection of the user's linked license IDs.
11066 *
11067 * @author Vova Feldman (@svovaf)
11068 * @since 2.0.0
11069 *
11070 * @param number $user_id
11071 *
11072 * @return number[]
11073 */
11074 private function get_user_linked_license_ids( $user_id ) {
11075 return self::get_user_id_license_ids_map( $this->_module_id, $user_id );
11076 }
11077
11078 /**
11079 * Override the user's linked license IDs with a new IDs collection.
11080 *
11081 * @author Vova Feldman (@svovaf)
11082 * @since 2.0.0
11083 *
11084 * @param number $user_id
11085 * @param number[] $license_ids
11086 */
11087 private function set_user_linked_license_ids( $user_id, array $license_ids ) {
11088 self::store_user_id_license_ids_map( $license_ids, $this->_module_id, $user_id );
11089 }
11090
11091 /**
11092 * Link a specified license ID to a given user.
11093 *
11094 * @author Vova Feldman (@svovaf)
11095 * @since 2.0.0
11096 *
11097 * @param number $license_id
11098 * @param number $user_id
11099 */
11100 private function link_license_2_user( $license_id, $user_id ) {
11101 $license_ids = $this->get_user_linked_license_ids( $user_id );
11102
11103 if ( in_array( $license_id, $license_ids ) ) {
11104 // License already linked.
11105 return;
11106 }
11107
11108 $license_ids[] = $license_id;
11109
11110 $this->set_user_linked_license_ids( $user_id, $license_ids );
11111 }
11112
11113 /**
11114 * @param string|bool $module_type
11115 *
11116 * @return FS_Plugin_Plan[]
11117 */
11118 private static function get_all_plans( $module_type = false ) {
11119 $plans = self::get_account_option( 'plans', $module_type );
11120
11121 if ( ! is_array( $plans ) ) {
11122 $plans = array();
11123 }
11124
11125 return $plans;
11126 }
11127
11128 /**
11129 * @author Vova Feldman (@svovaf)
11130 * @since 1.0.4
11131 *
11132 * @return FS_Plugin_Tag[]
11133 */
11134 private static function get_all_updates() {
11135 $updates = self::maybe_get_entities_account_option( 'updates', array() );
11136
11137 if ( ! is_array( $updates ) ) {
11138 $updates = array();
11139 }
11140
11141 return $updates;
11142 }
11143
11144 /**
11145 * @author Vova Feldman (@svovaf)
11146 * @since 1.0.6
11147 *
11148 * @return array<number,FS_Plugin[]>|false
11149 */
11150 private static function get_all_addons() {
11151 $addons = self::maybe_get_entities_account_option( 'addons', array() );
11152
11153 if ( ! is_array( $addons ) ) {
11154 $addons = array();
11155 }
11156
11157 return $addons;
11158 }
11159
11160 /**
11161 * @author Vova Feldman (@svovaf)
11162 * @since 1.0.6
11163 *
11164 * @return number[]|false
11165 */
11166 private static function get_all_account_addons() {
11167 $addons = self::$_accounts->get_option( 'account_addons', array() );
11168
11169 if ( ! is_array( $addons ) ) {
11170 $addons = array();
11171 }
11172
11173 return $addons;
11174 }
11175
11176 /**
11177 * Check if user has connected his account (opted-in).
11178 *
11179 * Note:
11180 * If the user opted-in and opted-out on a later stage,
11181 * this will still return true. If you want to check if the
11182 * user is currently opted-in, use:
11183 * `$fs->is_registered() && $fs->is_tracking_allowed()`
11184 *
11185 * @author Vova Feldman (@svovaf)
11186 * @since 1.0.1
11187 *
11188 * @param bool $ignore_anonymous_state Since 2.5.1
11189 *
11190 * @return bool
11191 */
11192 function is_registered( $ignore_anonymous_state = false ) {
11193 return (
11194 is_object( $this->_user ) &&
11195 (
11196 $this->is_premium() ||
11197 $ignore_anonymous_state ||
11198 ! $this->is_anonymous()
11199 )
11200 );
11201 }
11202
11203 /**
11204 * Returns TRUE if the user opted-in and didn't disconnect (opt-out).
11205 *
11206 * @author Leo Fajardo (@leorw)
11207 * @since 1.2.1.5
11208 *
11209 * @return bool
11210 */
11211 function is_tracking_allowed( $blog_id = null, $install = null ) {
11212 if ( is_null( $install ) ) {
11213 $install = is_null( $blog_id ) ?
11214 $this->_site :
11215 $this->get_install_by_blog_id( $blog_id );
11216 }
11217
11218 return (
11219 is_object( $install ) &&
11220 FS_Permission_Manager::instance( $this )->is_homepage_url_tracking_allowed( $blog_id )
11221 );
11222 }
11223
11224 /**
11225 * Returns TRUE if the user never opted-in or manually opted-out.
11226 *
11227 * @author Vova Feldman (@svovaf)
11228 * @since 1.2.1.5
11229 *
11230 * @param int|null $blog_id
11231 *
11232 * @return bool
11233 */
11234 function is_tracking_prohibited( $blog_id = null ) {
11235 return (
11236 ! $this->is_registered( true ) ||
11237 ! $this->is_tracking_allowed( $blog_id )
11238 );
11239 }
11240
11241 /**
11242 * @author Leo Fajardo (@leorw)
11243 * @since 2.4.0
11244 *
11245 * @return bool
11246 */
11247 function is_bundle_license_auto_activation_enabled() {
11248 return $this->is_addon() ?
11249 ( is_object( $this->_parent ) && $this->_parent->is_bundle_license_auto_activation_enabled() ) :
11250 $this->_is_bundle_license_auto_activation_enabled;
11251 }
11252
11253 /**
11254 * @author Vova Feldman (@svovaf)
11255 * @since 1.0.4
11256 *
11257 * @return FS_Plugin
11258 */
11259 function get_plugin() {
11260 return $this->_plugin;
11261 }
11262
11263 /**
11264 * @author Vova Feldman (@svovaf)
11265 * @since 1.0.3
11266 *
11267 * @return FS_User
11268 */
11269 function get_user() {
11270 return $this->_user;
11271 }
11272
11273 /**
11274 * @author Vova Feldman (@svovaf)
11275 * @since 1.0.3
11276 *
11277 * @return FS_Site
11278 */
11279 function get_site() {
11280 return $this->_site;
11281 }
11282
11283 /**
11284 * @author Leo Fajardo (@leorw)
11285 * @since 2.5.0
11286 */
11287 function store_site( $site ) {
11288 $this->_site = $site;
11289 $this->_store_site( true );
11290 }
11291
11292 /**
11293 * 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).
11294 *
11295 * @author Leo Fajardo (@leorw)
11296 * @since 2.5.0
11297 */
11298 function delete_current_install( $back_up ) {
11299 // Back up and delete the unique ID.
11300 if ( $back_up ) {
11301 self::$_accounts->set_option( 'prev_unique_id', $this->get_anonymous_id() );
11302 }
11303
11304 self::$_accounts->set_option( 'unique_id', null );
11305
11306 if ( $back_up ) {
11307 // 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).
11308 $this->back_up_site();
11309 }
11310
11311 $this->_delete_site();
11312 $this->_site = null;
11313 }
11314
11315 /**
11316 * @author Leo Fajardo (@leorw)
11317 * @since 2.5.0
11318 */
11319 function restore_backup_site() {
11320 self::$_accounts->set_option(
11321 'unique_id',
11322 self::$_accounts->get_option( 'prev_unique_id' )
11323 );
11324
11325 $sites = self::get_all_sites( $this->_module_type, null, true );
11326 $this->store_site( clone $sites[ $this->_slug ] );
11327 }
11328
11329 /**
11330 * Get plugin add-ons.
11331 *
11332 * @author Vova Feldman (@svovaf)
11333 * @since 1.0.6
11334 *
11335 * @since 1.1.7.3 If not yet loaded, fetch data from the API.
11336 *
11337 * @param bool $flush
11338 *
11339 * @return FS_Plugin[]|false
11340 */
11341 function get_addons( $flush = false ) {
11342 $this->_logger->entrance();
11343
11344 if ( ! $this->_has_addons ) {
11345 return false;
11346 }
11347
11348 $addons = $this->sync_addons( $flush );
11349
11350 return ( ! is_array( $addons ) || empty( $addons ) ) ?
11351 false :
11352 $addons;
11353 }
11354
11355 /**
11356 * @author Vova Feldman (@svovaf)
11357 * @since 1.0.6
11358 *
11359 * @return number[]|false
11360 */
11361 function get_account_addons() {
11362 $this->_logger->entrance();
11363
11364 $addons = self::get_all_account_addons();
11365
11366 if ( ! is_array( $addons ) ||
11367 ! isset( $addons[ $this->_plugin->id ] ) ||
11368 ! is_array( $addons[ $this->_plugin->id ] ) ||
11369 0 === count( $addons[ $this->_plugin->id ] )
11370 ) {
11371 return false;
11372 }
11373
11374 return $addons[ $this->_plugin->id ];
11375 }
11376
11377 /**
11378 * Check if user has any
11379 *
11380 * @author Vova Feldman (@svovaf)
11381 * @since 1.1.6
11382 *
11383 * @return bool
11384 */
11385 function has_account_addons() {
11386 $addons = $this->get_account_addons();
11387
11388 return is_array( $addons ) && ( 0 < count( $addons ) );
11389 }
11390
11391
11392 /**
11393 * Get add-on by ID (from local data).
11394 *
11395 * @author Vova Feldman (@svovaf)
11396 * @since 1.0.6
11397 *
11398 * @param number $id
11399 *
11400 * @return FS_Plugin|false
11401 */
11402 function get_addon( $id ) {
11403 $this->_logger->entrance();
11404
11405 $addons = $this->get_addons();
11406
11407 if ( is_array( $addons ) ) {
11408 foreach ( $addons as $addon ) {
11409 if ( $id == $addon->id ) {
11410 return $addon;
11411 }
11412 }
11413 }
11414
11415 return false;
11416 }
11417
11418 /**
11419 * Get add-on by slug (from local data).
11420 *
11421 * @author Vova Feldman (@svovaf)
11422 * @since 1.0.6
11423 *
11424 * @param string $slug
11425 *
11426 * @param bool $flush
11427 *
11428 * @return FS_Plugin|false
11429 */
11430 function get_addon_by_slug( $slug, $flush = false ) {
11431 $this->_logger->entrance();
11432
11433 $addons = $this->get_addons( $flush );
11434
11435 if ( is_array( $addons ) ) {
11436 foreach ( $addons as $addon ) {
11437 if ( $slug === $addon->slug ) {
11438 return $addon;
11439 }
11440 }
11441 }
11442
11443 return false;
11444 }
11445
11446 /**
11447 * @var array<number,object[]> {
11448 * @key number Add-on ID.
11449 * @val object[] The add-on's plans and prices object.
11450 * }
11451 */
11452 private $plans_and_pricing_by_addon_id;
11453
11454 /**
11455 * @author Leo Fajardo (@leorw)
11456 * @since 2.3.0
11457 *
11458 * @return array<number,object[]> {
11459 * @key number Add-on ID.
11460 * @val object[] The add-on's plans and prices object.
11461 * }
11462 */
11463 function _get_addons_plans_and_pricing_map_by_id() {
11464 if ( ! isset( $this->plans_and_pricing_by_addon_id ) ) {
11465 $result = $this->get_api_plugin_scope()->get( $this->add_show_pending( "/addons/pricing.json?type=visible" ) );
11466
11467 $plans_and_pricing_by_addon_id = array();
11468 if ( $this->is_api_result_object( $result, 'addons' ) ) {
11469 foreach ( $result->addons as $addon ) {
11470 $plans_and_pricing_by_addon_id[ $addon->id ] = $addon->plans;
11471 }
11472 }
11473
11474 $this->plans_and_pricing_by_addon_id = $plans_and_pricing_by_addon_id;
11475 }
11476
11477 return $this->plans_and_pricing_by_addon_id;
11478 }
11479
11480 /**
11481 * @author Leo Fajardo (@leorw)
11482 * @since 2.3.0
11483 *
11484 * @param number $addon_id
11485 * @param bool $is_installed
11486 *
11487 * @return array
11488 */
11489 function _get_addon_info( $addon_id, $is_installed ) {
11490 $addon = $this->get_addon( $addon_id );
11491
11492 if ( ! is_object( $addon ) ) {
11493 // Unexpected call.
11494 return array();
11495 }
11496
11497 $slug = $addon->slug;
11498
11499 $addon_storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
11500
11501 if ( ! fs_is_network_admin() ) {
11502 // Get blog-level activated installations.
11503 $sites = self::maybe_get_entities_account_option( 'sites', array() );
11504 } else {
11505 $sites = null;
11506
11507 if ( $this->is_addon_activated( $addon_id ) &&
11508 $this->get_addon_instance( $addon_id )->is_network_active()
11509 ) {
11510 if ( FS_Site::is_valid_id( $addon_storage->network_install_blog_id ) ) {
11511 // Get network-level activated installations.
11512 $sites = self::maybe_get_entities_account_option(
11513 'sites',
11514 array(),
11515 $addon_storage->network_install_blog_id
11516 );
11517 }
11518 }
11519 }
11520
11521 $addon_info = array(
11522 'is_connected' => false,
11523 'slug' => $slug,
11524 'title' => $addon->title,
11525 'is_whitelabeled' => $addon_storage->is_whitelabeled
11526 );
11527
11528 if ( ! $is_installed ) {
11529 $plans_and_pricing_by_addon_id = $this->_get_addons_plans_and_pricing_map_by_id();
11530
11531 if ( isset( $plans_and_pricing_by_addon_id[ $addon_id ] ) ) {
11532 $has_paid_plan = false;
11533 $plans = $plans_and_pricing_by_addon_id[ $addon_id ];
11534
11535 if ( is_array( $plans ) && count( $plans ) > 0 ) {
11536 foreach ( $plans as $plan ) {
11537 if ( isset( $plan->pricing ) &&
11538 is_array( $plan->pricing ) &&
11539 count( $plan->pricing ) > 0
11540 ) {
11541 $has_paid_plan = true;
11542 break;
11543 }
11544 }
11545 }
11546
11547 $addon_info['has_paid_plan'] = $has_paid_plan;
11548 }
11549 }
11550
11551 if ( ! is_array( $sites ) || ! isset( $sites[ $slug ] ) ) {
11552 return $addon_info;
11553 }
11554
11555 $site = $sites[ $slug ];
11556
11557 $addon_info['is_connected'] = (
11558 ( $addon->parent_plugin_id == $this->get_id() ) &&
11559 is_object( $site ) &&
11560 FS_Site::is_valid_id( $site->id ) &&
11561 FS_User::is_valid_id( $site->user_id ) &&
11562 FS_Plugin_Plan::is_valid_id( $site->plan_id )
11563 );
11564
11565 if ( $addon_info['is_connected'] && $is_installed ) {
11566 return $addon_info;
11567 }
11568
11569 $addon_info['site'] = $site;
11570
11571 $plugins_data = self::maybe_get_entities_account_option( WP_FS__MODULE_TYPE_PLUGIN . 's', array() );
11572 if ( isset( $plugins_data[ $slug ] ) ) {
11573 $plugin_data = $plugins_data[ $slug ];
11574
11575 $addon_info['version'] = $plugin_data->version;
11576 }
11577
11578 $all_plans = self::maybe_get_entities_account_option( 'plans', array() );
11579 if ( isset( $all_plans[ $slug ] ) ) {
11580 $plans = $all_plans[ $slug ];
11581
11582 foreach ( $plans as $plan ) {
11583 if ( $site->plan_id == Freemius::_decrypt( $plan->id ) ) {
11584 $addon_info['plan_name'] = Freemius::_decrypt( $plan->name );
11585 $addon_info['plan_title'] = Freemius::_decrypt( $plan->title );
11586 break;
11587 }
11588 }
11589 }
11590
11591 $licenses = self::maybe_get_entities_account_option( 'all_licenses', array() );
11592 if ( is_array( $licenses ) && isset( $licenses[ $addon_id ] ) ) {
11593 foreach ( $licenses[ $addon_id ] as $license ) {
11594 if ( $license->id == $site->license_id ) {
11595 $addon_info['license'] = $license;
11596 break;
11597 }
11598 }
11599 }
11600
11601 if ( isset( $addon_info['license'] ) ) {
11602 if ( isset( $addon_storage->subscriptions ) &&
11603 ! empty( $addon_storage->subscriptions )
11604 ) {
11605 $addon_subscriptions = fs_get_entities( $addon_storage->subscriptions, FS_Subscription::get_class_name() );
11606
11607 foreach ( $addon_subscriptions as $subscription ) {
11608 if ( $subscription->license_id == $site->license_id ) {
11609 $addon_info['subscription'] = $subscription;
11610 break;
11611 }
11612 }
11613 }
11614 }
11615
11616 return $addon_info;
11617 }
11618
11619 /**
11620 * @author Vova Feldman (@svovaf)
11621 * @since 2.0.0
11622 *
11623 * @param number $user_id
11624 *
11625 * @return FS_User
11626 */
11627 static function _get_user_by_id( $user_id ) {
11628 self::$_static_logger->entrance( "user_id = {$user_id}" );
11629
11630 $users = self::get_all_users();
11631
11632 if ( is_array( $users ) ) {
11633 if ( isset( $users[ $user_id ] ) &&
11634 $users[ $user_id ] instanceof FS_User &&
11635 $user_id == $users[ $user_id ]->id
11636 ) {
11637 return $users[ $user_id ];
11638 }
11639
11640 // If user wasn't found by the key, iterate over all the users collection.
11641 foreach ( $users as $user ) {
11642 /**
11643 * @var FS_User $user
11644 */
11645 if ( $user_id == $user->id ) {
11646 return $user;
11647 }
11648 }
11649 }
11650
11651 return null;
11652 }
11653
11654 /**
11655 * Checks if a Freemius user_id is associated with a super-admin.
11656 *
11657 * @author Vova Feldman (@svovaf)
11658 * @since 2.0.0
11659 *
11660 * @param number $user_id
11661 *
11662 * @return bool
11663 */
11664 private static function is_super_admin( $user_id ) {
11665 $is_super_admin = false;
11666
11667 $user = self::_get_user_by_id( $user_id );
11668
11669 if ( $user instanceof FS_User && ! empty( $user->email ) ) {
11670 self::require_pluggable_essentials();
11671
11672 $wp_user = get_user_by( 'email', $user->email );
11673
11674 if ( $wp_user instanceof WP_User ) {
11675 $super_admins = get_super_admins();
11676 $is_super_admin = ( is_array( $super_admins ) && in_array( $wp_user->user_login, $super_admins ) );
11677 }
11678 }
11679
11680 return $is_super_admin;
11681 }
11682
11683 #----------------------------------------------------------------------------------
11684 #region Plans & Licensing
11685 #----------------------------------------------------------------------------------
11686
11687 /**
11688 * Check if running premium plugin code.
11689 *
11690 * @author Vova Feldman (@svovaf)
11691 * @since 1.0.5
11692 *
11693 * @return bool
11694 */
11695 function is_premium() {
11696 /**
11697 * `$this->_plugin` will be `false` when `is_activation_mode` calls this method directly from the
11698 * `register_constructor_hooks` method.
11699 *
11700 * @author Leo Fajardo (@leorw)
11701 * @since 2.2.3
11702 */
11703 return is_object( $this->_plugin ) ?
11704 $this->_plugin->is_premium :
11705 false;
11706 }
11707
11708 /**
11709 * Get site's plan ID.
11710 *
11711 * @author Vova Feldman (@svovaf)
11712 * @since 1.0.2
11713 *
11714 * @return number
11715 */
11716 function get_plan_id() {
11717 return $this->_site->plan_id;
11718 }
11719
11720 /**
11721 * Get site's plan title.
11722 *
11723 * @author Vova Feldman (@svovaf)
11724 * @since 1.0.2
11725 *
11726 * @return string
11727 */
11728 function get_plan_title() {
11729 $plan = $this->get_plan();
11730
11731 return is_object( $plan ) ? $plan->title : 'PLAN_TITLE';
11732 }
11733
11734 /**
11735 * Get site's plan name.
11736 *
11737 * @author Vova Feldman (@svovaf)
11738 * @since 2.0.0
11739 *
11740 * @return string
11741 */
11742 function get_plan_name() {
11743 $plan = $this->get_plan();
11744
11745 return is_object( $plan ) ? $plan->name : 'PLAN_NAME';
11746 }
11747
11748 /**
11749 * @author Vova Feldman (@svovaf)
11750 * @since 1.0.9
11751 *
11752 * @return FS_Plugin_Plan|false
11753 */
11754 function get_plan() {
11755 if ( ! is_object( $this->_site ) ) {
11756 return false;
11757 }
11758
11759 return FS_Plugin_Plan::is_valid_id( $this->_site->plan_id ) ?
11760 $this->_get_plan_by_id( $this->_site->plan_id ) :
11761 false;
11762 }
11763
11764 /**
11765 * @author Vova Feldman (@svovaf)
11766 * @since 1.0.3
11767 *
11768 * @return bool
11769 */
11770 function is_trial() {
11771 $this->_logger->entrance();
11772
11773 if ( ! $this->is_registered() || ! is_object( $this->_site ) ) {
11774 return false;
11775 }
11776
11777 return $this->_site->is_trial();
11778 }
11779
11780 /**
11781 * Check if currently in a trial with payment method (credit card or paypal).
11782 *
11783 * @author Vova Feldman (@svovaf)
11784 * @since 1.1.7
11785 *
11786 * @return bool
11787 */
11788 function is_paid_trial() {
11789 $this->_logger->entrance();
11790
11791 if ( ! $this->is_trial() ) {
11792 return false;
11793 }
11794
11795 if ( ! $this->has_active_valid_license() ) {
11796 return false;
11797 }
11798
11799 if ( $this->_site->trial_plan_id != $this->_license->plan_id ) {
11800 return false;
11801 }
11802
11803 /**
11804 * @var FS_Subscription $subscription
11805 */
11806 $subscription = $this->_get_subscription( $this->_license->id );
11807
11808 return ( is_object( $subscription ) && $subscription->is_active() );
11809 }
11810
11811 /**
11812 * Check if trial already utilized.
11813 *
11814 * @since 1.0.9
11815 *
11816 * @return bool
11817 */
11818 function is_trial_utilized() {
11819 $this->_logger->entrance();
11820
11821 if ( ! $this->is_registered() ) {
11822 return false;
11823 }
11824
11825 return $this->_site->is_trial_utilized();
11826 }
11827
11828 /**
11829 * Get trial plan information (if in trial).
11830 *
11831 * @author Vova Feldman (@svovaf)
11832 * @since 1.0.9
11833 *
11834 * @return bool|FS_Plugin_Plan
11835 */
11836 function get_trial_plan() {
11837 $this->_logger->entrance();
11838
11839 if ( ! $this->is_trial() ) {
11840 return false;
11841 }
11842
11843 // Try to load plan from local cache.
11844 $trial_plan = $this->_get_plan_by_id( $this->_site->trial_plan_id );
11845
11846 if ( ! is_object( $trial_plan ) ) {
11847 $trial_plan = $this->_fetch_site_plan( $this->_site->trial_plan_id );
11848
11849 /**
11850 * If managed to fetch the plan, add it to the plans collection.
11851 */
11852 if ( $trial_plan instanceof FS_Plugin_Plan ) {
11853 if ( ! is_array( $this->_plans ) ) {
11854 $this->_plans = array();
11855 }
11856
11857 $this->_plans[] = $trial_plan;
11858 $this->_store_plans();
11859 }
11860 }
11861
11862 if ( $trial_plan instanceof FS_Plugin_Plan ) {
11863 return $trial_plan;
11864 }
11865
11866 /**
11867 * If for some reason failed to get the trial plan, fallback to a dummy name and title.
11868 */
11869 $trial_plan = new FS_Plugin_Plan();
11870 $trial_plan->id = $this->_site->trial_plan_id;
11871 $trial_plan->name = 'pro';
11872 $trial_plan->title = 'Pro';
11873
11874 return $trial_plan;
11875 }
11876
11877 /**
11878 * Check if the user has an activate, non-expired license on current plugin's install.
11879 *
11880 * @since 1.0.9
11881 *
11882 * @return bool
11883 */
11884 function is_paying() {
11885 $this->_logger->entrance();
11886
11887 if ( ! $this->is_registered() ) {
11888 return false;
11889 }
11890
11891 if ( ! $this->has_paid_plan() ) {
11892 return false;
11893 }
11894
11895 return (
11896 ! $this->is_trial() &&
11897 'free' !== $this->get_plan_name() &&
11898 $this->has_active_valid_license()
11899 );
11900 }
11901
11902 /**
11903 * @author Vova Feldman (@svovaf)
11904 * @since 1.0.4
11905 *
11906 * @return bool
11907 */
11908 function is_free_plan() {
11909 if ( ! $this->is_registered() ) {
11910 return true;
11911 }
11912
11913 if ( ! $this->has_paid_plan() ) {
11914 return true;
11915 }
11916
11917 return (
11918 'free' === $this->get_plan_name() ||
11919 ! $this->has_features_enabled_license()
11920 );
11921 }
11922
11923 /**
11924 * @author Vova Feldman (@svovaf)
11925 * @since 1.0.5
11926 *
11927 * @return bool
11928 */
11929 function _has_premium_license() {
11930 $this->_logger->entrance();
11931
11932 $premium_license = $this->_get_available_premium_license();
11933
11934 return ( false !== $premium_license );
11935 }
11936
11937 /**
11938 * Check if user has any licenses associated with the plugin (including expired or blocking).
11939 *
11940 * @author Vova Feldman (@svovaf)
11941 * @since 1.1.7.3
11942 *
11943 * @param bool $including_foreign
11944 *
11945 * @return bool
11946 */
11947 function has_any_license( $including_foreign = true ) {
11948 if ( ! is_array( $this->_licenses ) || 0 === count( $this->_licenses ) ) {
11949 return false;
11950 }
11951
11952 if ( $including_foreign ) {
11953 return true;
11954 }
11955
11956 foreach ( $this->_licenses as $license ) {
11957 if ( $this->_user->id == $license->user_id ) {
11958 return true;
11959 }
11960 }
11961
11962 return false;
11963 }
11964
11965 /**
11966 * @author Vova Feldman (@svovaf)
11967 * @since 1.0.5
11968 *
11969 * @param bool|null $is_localhost
11970 *
11971 * @return FS_Plugin_License|false
11972 */
11973 function _get_available_premium_license( $is_localhost = null ) {
11974 $this->_logger->entrance();
11975
11976 $licenses = $this->get_available_premium_licenses( $is_localhost );
11977 if ( ! empty( $licenses ) ) {
11978 return $licenses[0];
11979 }
11980
11981 return false;
11982 }
11983
11984 /**
11985 * @author Vova Feldman (@svovaf)
11986 * @since 1.0.5
11987 *
11988 * @param bool|null $is_localhost
11989 *
11990 * @return FS_Plugin_License[]
11991 */
11992 function get_available_premium_licenses( $is_localhost = null ) {
11993 $this->_logger->entrance();
11994
11995 $licenses = array();
11996 if ( ! $this->has_paid_plan() ) {
11997 return $licenses;
11998 }
11999
12000 if ( is_array( $this->_licenses ) ) {
12001 foreach ( $this->_licenses as $license ) {
12002 if ( ! $license->can_activate( $is_localhost ) ) {
12003 continue;
12004 }
12005
12006 $licenses[] = $license;
12007 }
12008 }
12009
12010 return $licenses;
12011 }
12012
12013 /**
12014 * Sync local plugin plans with remote server.
12015 *
12016 * 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).
12017 *
12018 * @author Vova Feldman (@svovaf)
12019 * @since 1.0.5
12020 *
12021 * @return FS_Plugin_Plan[]|object
12022 */
12023 function _sync_plans() {
12024 $plans = $this->_fetch_plugin_plans();
12025
12026 if ( $this->is_array_instanceof( $plans, 'FS_Plugin_Plan' ) ) {
12027 $plans_map = array();
12028 foreach ( $plans as $plan ) {
12029 $plans_map[ $plan->id ] = true;
12030 }
12031
12032 $plans_ids_to_keep = $this->get_plans_ids_associated_with_installs();
12033
12034 foreach ( $plans_ids_to_keep as $plan_id ) {
12035 if ( isset( $plans_map[ $plan_id ] ) ) {
12036 continue;
12037 }
12038
12039 $missing_plan = self::_get_plan_by_id( $plan_id );
12040
12041 if ( is_object( $missing_plan ) ) {
12042 $plans[] = $missing_plan;
12043 }
12044 }
12045
12046 $this->_plans = $plans;
12047 $this->_store_plans();
12048 }
12049
12050 $this->do_action( 'after_plans_sync', $plans );
12051
12052 return $this->_plans;
12053 }
12054
12055 /**
12056 * Check if specified plan exists locally. If not, fetch it and store it.
12057 *
12058 * @author Vova Feldman (@svovaf)
12059 * @since 2.0.0
12060 *
12061 * @param number $plan_id
12062 *
12063 * @return \FS_Plugin_Plan|object The plan entity or the API error object on failure.
12064 */
12065 private function sync_plan_if_not_exist( $plan_id ) {
12066 $plan = self::_get_plan_by_id( $plan_id );
12067
12068 if ( is_object( $plan ) ) {
12069 // Plan already exists.
12070 return $plan;
12071 }
12072
12073 $plan = $this->fetch_plan_by_id( $plan_id );
12074
12075 if ( $plan instanceof FS_Plugin_Plan ) {
12076 $this->_plans[] = $plan;
12077 $this->_store_plans();
12078
12079 return $plan;
12080 }
12081
12082 return $plan;
12083 }
12084
12085 /**
12086 * Check if specified license exists locally. If not, fetch it and store it.
12087 *
12088 * @author Vova Feldman (@svovaf)
12089 * @since 2.0.0
12090 *
12091 * @param number $license_id
12092 * @param string $license_key
12093 *
12094 * @return \FS_Plugin_Plan|object The plan entity or the API error object on failure.
12095 */
12096 private function sync_license_if_not_exist( $license_id, $license_key ) {
12097 $license = $this->_get_license_by_id( $license_id );
12098
12099 if ( is_object( $license ) ) {
12100 // License already exists.
12101 return $license;
12102 }
12103
12104 $license = $this->fetch_license_by_key( $license_id, $license_key );
12105
12106 if ( $license instanceof FS_Plugin_License ) {
12107 $this->_licenses[] = $license;
12108
12109 $this->set_license( $license );
12110
12111 $this->_store_licenses();
12112
12113 return $license;
12114 }
12115
12116 return $license;
12117 }
12118
12119 /**
12120 * Get a collection of unique plan IDs that are associated with any installs in the network.
12121 *
12122 * @author Leo Fajardo (@leorw)
12123 * @since 2.0.0
12124 *
12125 * @return number[]
12126 */
12127 private function get_plans_ids_associated_with_installs() {
12128 if ( ! is_multisite() ) {
12129 if ( ! is_object( $this->_site ) ||
12130 ! FS_Plugin_Plan::is_valid_id( $this->_site->plan_id )
12131 ) {
12132 return array();
12133 }
12134
12135 return array( $this->_site->plan_id );
12136 }
12137
12138 $plan_ids = array();
12139 $sites = self::get_sites();
12140 foreach ( $sites as $site ) {
12141 $blog_id = self::get_site_blog_id( $site );
12142 $install = $this->get_install_by_blog_id( $blog_id );
12143
12144 if ( ! is_object( $install ) ||
12145 ! FS_Plugin_Plan::is_valid_id( $install->plan_id )
12146 ) {
12147 continue;
12148 }
12149
12150 $plan_ids[ $install->plan_id ] = true;
12151 }
12152
12153 return array_keys( $plan_ids );
12154 }
12155
12156 /**
12157 * Get a collection of unique license IDs that are associated with any installs in the network.
12158 *
12159 * @author Leo Fajardo (@leorw)
12160 * @since 2.0.0
12161 *
12162 * @return number[]
12163 */
12164 private function get_license_ids_associated_with_installs() {
12165 if ( ! $this->_is_network_active ) {
12166 if ( ! is_object( $this->_site ) ||
12167 ! FS_Plugin_License::is_valid_id( $this->_site->license_id )
12168 ) {
12169 return array();
12170 }
12171
12172 return array( $this->_site->license_id );
12173 }
12174
12175 $license_ids = array();
12176 $sites = self::get_sites();
12177 foreach ( $sites as $site ) {
12178 $blog_id = self::get_site_blog_id( $site );
12179 $install = $this->get_install_by_blog_id( $blog_id );
12180
12181 if ( ! is_object( $install ) ||
12182 ! FS_Plugin_License::is_valid_id( $install->license_id )
12183 ) {
12184 continue;
12185 }
12186
12187 $license_ids[ $install->license_id ] = true;
12188 }
12189
12190 return array_keys( $license_ids );
12191 }
12192
12193 /**
12194 * @author Vova Feldman (@svovaf)
12195 * @since 1.0.5
12196 *
12197 * @param number $id
12198 *
12199 * @return FS_Plugin_Plan|false
12200 */
12201 function _get_plan_by_id( $id ) {
12202 $this->_logger->entrance();
12203
12204 if ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) {
12205 $this->_sync_plans();
12206 }
12207
12208 foreach ( $this->_plans as $plan ) {
12209 if ( $id == $plan->id ) {
12210 return $plan;
12211 }
12212 }
12213
12214 return false;
12215 }
12216
12217 /**
12218 * @author Vova Feldman (@svovaf)
12219 * @since 1.1.8.1
12220 *
12221 * @param string $name
12222 *
12223 * @return FS_Plugin_Plan|false
12224 */
12225 private function get_plan_by_name( $name ) {
12226 $this->_logger->entrance();
12227
12228 if ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) {
12229 $this->_sync_plans();
12230 }
12231
12232 foreach ( $this->_plans as $plan ) {
12233 if ( $name == $plan->name ) {
12234 return $plan;
12235 }
12236 }
12237
12238 return false;
12239 }
12240
12241 /**
12242 * Sync local licenses with remote server.
12243 *
12244 * @author Vova Feldman (@svovaf)
12245 * @since 1.0.6
12246 *
12247 * @param number|bool $site_license_id
12248 * @param number|null $blog_id
12249 *
12250 * @return FS_Plugin_License[]|object
12251 */
12252 function _sync_licenses( $site_license_id = false, $blog_id = null ) {
12253 $this->_logger->entrance();
12254
12255 $is_network_admin = fs_is_network_admin();
12256
12257 if ( $is_network_admin && is_null( $blog_id ) ) {
12258 $all_licenses = self::get_all_licenses( $this->_module_id );
12259 } else {
12260 $all_licenses = $this->get_user_licenses( $this->_user->id );
12261 }
12262
12263 $foreign_licenses = $this->get_foreign_licenses_info( $all_licenses, $site_license_id );
12264
12265 $all_licenses_map = array();
12266 foreach ( $all_licenses as $license ) {
12267 $all_licenses_map[ $license->id ] = true;
12268 }
12269
12270 $licenses = $this->_fetch_licenses( false, $site_license_id, $foreign_licenses, $blog_id );
12271
12272 if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) {
12273 $licenses_map = array();
12274 foreach ( $licenses as $license ) {
12275 $licenses_map[ $license->id ] = true;
12276 }
12277
12278 // $license_ids_to_keep = $this->get_license_ids_associated_with_installs();
12279 // foreach ( $license_ids_to_keep as $license_id ) {
12280 // if ( isset( $licenses_map[ $license_id ] ) ) {
12281 // continue;
12282 // }
12283 //
12284 // $missing_license = self::_get_license_by_id( $license_id, false );
12285 // if ( is_object( $missing_license ) ) {
12286 // $licenses[] = $missing_license;
12287 // $licenses_map[ $missing_license->id ] = true;
12288 // }
12289 // }
12290
12291 $user_license_ids = $this->get_user_linked_license_ids( $this->_user->id );
12292
12293 foreach ( $user_license_ids as $key => $license_id ) {
12294 if ( ! isset( $licenses_map[ $license_id ] ) ) {
12295 // Remove access to licenses that no longer exist.
12296 unset( $user_license_ids[ $key ] );
12297 }
12298 }
12299
12300 if ( ! empty( $user_license_ids ) ) {
12301 foreach ( $licenses_map as $license_id => $value ) {
12302 if ( ! isset( $all_licenses_map[ $license_id ] ) ) {
12303 // Associate new licenses with the user who triggered the license syncing.
12304 $user_license_ids[] = $license_id;
12305 }
12306 }
12307
12308 $user_license_ids = array_unique( $user_license_ids );
12309 } else {
12310 $user_license_ids = array_keys( $licenses_map );
12311 }
12312
12313 if ( ! $is_network_admin || ! is_null( $blog_id ) ) {
12314 $user_licenses = array();
12315 foreach ( $licenses as $license ) {
12316 if ( ! in_array( $license->id, $user_license_ids ) ) {
12317 continue;
12318 }
12319
12320 $user_licenses[] = $license;
12321 }
12322
12323 $this->_licenses = $user_licenses;
12324 } else {
12325 $this->_licenses = $licenses;
12326 }
12327
12328 $this->set_user_linked_license_ids( $this->_user->id, $user_license_ids );
12329
12330 $this->_store_licenses( true, $this->_module_id, $licenses );
12331 }
12332
12333 // Update current license.
12334 if ( is_object( $this->_license ) ) {
12335 $license = $this->_get_license_by_id( $this->_license->id );
12336
12337 if ( is_object( $license ) ) {
12338 /**
12339 * `$license` can be `false` in case a user change action has just been completed and this method
12340 * has synced the `$this->_licenses` collection for the new user. In this case, the
12341 * `$this->_licenses` collection may have only the newly activated license that is associated with
12342 * the new user. `set_license` will eventually be called in the same request by the logic that
12343 * follows outside this method which will detect that the install's license has been updated, and
12344 * then `_update_site_license` will be called which in turn will call `set_license`.
12345 *
12346 * @author Leo Fajardo (@leorw)
12347 * @since 2.3.2
12348 */
12349 $this->set_license( $license );
12350 }
12351 }
12352
12353 return $this->_licenses;
12354 }
12355
12356 /**
12357 * @author Vova Feldman (@svovaf)
12358 * @since 1.0.5
12359 *
12360 * @param number $id
12361 * @param bool $sync_licenses
12362 *
12363 * @return FS_Plugin_License|false
12364 */
12365 function _get_license_by_id( $id, $sync_licenses = true ) {
12366 $this->_logger->entrance();
12367
12368 if ( ! FS_Plugin_License::is_valid_id( $id ) ) {
12369 return false;
12370 }
12371
12372 /**
12373 * When running from the network level admin and opted-in from the network,
12374 * check if the license exists in the network user licenses collection.
12375 *
12376 * @author Vova Feldman (@svovaf)
12377 * @since 2.0.0
12378 */
12379 if ( fs_is_network_admin() &&
12380 $this->is_network_registered() &&
12381 ( ! is_object( $this->_user ) || $this->_storage->network_user_id != $this->_user->id )
12382 ) {
12383 $licenses = $this->get_user_licenses( $this->_storage->network_user_id );
12384
12385 foreach ( $licenses as $license ) {
12386 if ( $id == $license->id ) {
12387 return $license;
12388 }
12389 }
12390 }
12391
12392 if ( ! $this->has_any_license() && $sync_licenses ) {
12393 $this->_sync_licenses( $id );
12394 }
12395
12396 if ( is_array( $this->_licenses ) ) {
12397 foreach ( $this->_licenses as $license ) {
12398 if ( $id == $license->id ) {
12399 return $license;
12400 }
12401 }
12402 }
12403
12404 return false;
12405 }
12406
12407 /**
12408 * 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.
12409 *
12410 * @author Vova Feldman (@svovaf)
12411 * @since 2.0.0
12412 *
12413 * @param number $id
12414 *
12415 * @return FS_Plugin_License
12416 */
12417 private function get_license_by_id( $id ) {
12418 $licenses = self::get_all_licenses( $this->_module_id );
12419
12420 if ( is_array( $licenses ) && ! empty( $licenses ) ) {
12421 foreach ( $licenses as $license ) {
12422 if ( $id == $license->id ) {
12423 return $license;
12424 }
12425 }
12426 }
12427
12428 return null;
12429 }
12430
12431 /**
12432 * Synchronize the site's context license by fetching the license form the API and updating the local data with it.
12433 *
12434 * @author Vova Feldman (@svovaf)
12435 * @since 2.0.0
12436 *
12437 * @return \FS_Plugin_License|mixed
12438 */
12439 private function sync_site_license() {
12440 $api = $this->get_api_user_scope();
12441
12442 $result = $api->get( "/licenses/{$this->_license->id}.json?license_key=" . urlencode( $this->_license->secret_key ), true );
12443
12444 if ( ! $this->is_api_result_entity( $result ) ) {
12445 return $result;
12446 }
12447
12448 $license = $this->_update_site_license( new FS_Plugin_License( $result ) );
12449 $this->_store_licenses();
12450
12451 return $license;
12452 }
12453
12454 /**
12455 * Get all user's available licenses for the current module.
12456 *
12457 * @author Vova Feldman (@svovaf)
12458 * @since 2.0.0
12459 *
12460 * @param number $user_id
12461 *
12462 * @return FS_Plugin_License[]
12463 */
12464 private function get_user_licenses( $user_id ) {
12465 $all_licenses = self::get_all_licenses( $this->_module_id );
12466 if ( empty( $all_licenses ) ) {
12467 return array();
12468 }
12469
12470 $user_license_ids = $this->get_user_linked_license_ids( $user_id );
12471 if ( empty( $user_license_ids ) ) {
12472 return array();
12473 }
12474
12475 $licenses = array();
12476 foreach ( $all_licenses as $license ) {
12477 if ( in_array( $license->id, $user_license_ids ) ) {
12478 $licenses[] = $license;
12479 }
12480 }
12481
12482 return $licenses;
12483 }
12484
12485 /**
12486 * Checks if the context license is network activated except on the given blog ID.
12487 *
12488 * @author Vova Feldman (@svovaf)
12489 * @since 2.0.0
12490 *
12491 * @param int $except_blog_id
12492 *
12493 * @return bool
12494 */
12495 private function is_license_network_active( $except_blog_id = 0 ) {
12496 $this->_logger->entrance();
12497
12498 if ( ! is_object( $this->_license ) ) {
12499 return false;
12500 }
12501
12502 $sites = self::get_sites();
12503
12504 if ( $this->_license->total_activations() < ( count( $sites ) - 1 ) ) {
12505 // There are more sites than the number of activations, so license cannot be network activated.
12506 return false;
12507 }
12508
12509 foreach ( $sites as $site ) {
12510 $blog_id = self::get_site_blog_id( $site );
12511
12512 if ( $except_blog_id == $blog_id ) {
12513 // Skip excluded blog.
12514 continue;
12515 }
12516
12517 $install = $this->get_install_by_blog_id( $blog_id );
12518
12519 if ( is_object( $install ) && $install->license_id != $this->_license->id ) {
12520 return false;
12521 }
12522 }
12523
12524 return true;
12525 }
12526
12527 /**
12528 * 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.
12529 *
12530 * Notice: On success, this method will also update the license activations counters (without updating the license in the storage).
12531 *
12532 * @author Vova Feldman (@svovaf)
12533 * @since 2.0.0
12534 *
12535 * @param \FS_User $user
12536 * @param \FS_Plugin_License $license
12537 *
12538 * @return bool
12539 */
12540 private function try_activate_license_on_network( FS_User $user, FS_Plugin_License $license ) {
12541 $this->_logger->entrance();
12542
12543 $result = $this->can_activate_license_on_network( $license );
12544
12545 if ( false === $result ) {
12546 return false;
12547 }
12548
12549 $installs_without_license = $result['installs'];
12550 if ( ! empty( $installs_without_license ) ) {
12551 $this->activate_license_on_many_installs( $user, $license->secret_key, $installs_without_license );
12552 }
12553
12554 $disconnected_site_ids = $result['sites'];
12555 if ( ! empty( $disconnected_site_ids ) ) {
12556 $this->activate_license_on_many_sites( $user, $license->secret_key, $disconnected_site_ids );
12557 }
12558
12559 $this->link_license_2_user( $license->id, $user->id );
12560
12561 // Sync license after activations.
12562 $license->activated += $result['production_count'];
12563 $license->activated_local += $result['localhost_count'];
12564
12565 // $this->_store_licenses()
12566
12567 return true;
12568 }
12569
12570 /**
12571 * Checks if the given license can be activated on the whole network.
12572 *
12573 * @author Vova Feldman (@svovaf)
12574 * @since 2.0.0
12575 *
12576 * @param \FS_Plugin_License $license
12577 *
12578 * @return false|array {
12579 * @type array[int]FS_Site $installs Blog ID to install map.
12580 * @type int[] $sites Non-connected blog IDs.
12581 * @type int $production_count Production sites count.
12582 * @type int $localhost_count Production sites count.
12583 * }
12584 */
12585 private function can_activate_license_on_network( FS_Plugin_License $license ) {
12586 $sites = self::get_sites();
12587
12588 $production_count = 0;
12589 $localhost_count = 0;
12590
12591 $installs_without_license = array();
12592 $disconnected_site_ids = array();
12593
12594 foreach ( $sites as $site ) {
12595 $blog_id = self::get_site_blog_id( $site );
12596 $install = $this->get_install_by_blog_id( $blog_id );
12597
12598 if ( is_object( $install ) ) {
12599 if ( FS_Plugin_License::is_valid_id( $install->license_id ) ) {
12600 // License already activated on the install.
12601 continue;
12602 }
12603
12604 $url = $install->url;
12605
12606 $installs_without_license[ $blog_id ] = $install;
12607 } else {
12608 $url = is_object( $site ) ?
12609 $site->siteurl :
12610 self::get_unfiltered_site_url( $blog_id );
12611
12612 $disconnected_site_ids[] = $blog_id;
12613 }
12614
12615 if ( FS_Site::is_localhost_by_address( $url ) ) {
12616 $localhost_count ++;
12617 } else {
12618 $production_count ++;
12619 }
12620 }
12621
12622 if ( ! $license->can_activate_bulk( $production_count, $localhost_count ) ) {
12623 return false;
12624 }
12625
12626 return array(
12627 'installs' => $installs_without_license,
12628 'sites' => $disconnected_site_ids,
12629 'production_count' => $production_count,
12630 'localhost_count' => $localhost_count,
12631 );
12632 }
12633
12634 /**
12635 * Activate a given license on a collection of installs.
12636 *
12637 * @author Vova Feldman (@svovaf)
12638 * @since 2.0.0
12639 *
12640 * @param \FS_User $user
12641 * @param string $license_key
12642 * @param array $blog_2_install_map {
12643 * @key int Blog ID.
12644 * @value FS_Site Blog's associated install.
12645 * }
12646 *
12647 * @return mixed|true
12648 */
12649 private function activate_license_on_many_installs(
12650 FS_User $user,
12651 $license_key,
12652 array $blog_2_install_map
12653 ) {
12654 $params = array(
12655 array( 'license_key' => $this->apply_filters( 'license_key', $license_key ) )
12656 );
12657
12658 $install_2_blog_map = array();
12659 foreach ( $blog_2_install_map as $blog_id => $install ) {
12660 $params[] = array( 'id' => $install->id );
12661
12662 $install_2_blog_map[ $install->id ] = $blog_id;
12663 }
12664
12665 $result = $this->get_api_user_scope_by_user( $user )->call(
12666 "plugins/{$this->_plugin->id}/installs.json",
12667 'PUT',
12668 $params
12669 );
12670
12671 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
12672 return $result;
12673 }
12674
12675 foreach ( $result->installs as $r_install ) {
12676 $install = new FS_Site( $r_install );
12677 $install->is_disconnected = false;
12678
12679 // Update install.
12680 $this->_store_site(
12681 true,
12682 $install_2_blog_map[ $r_install->id ],
12683 $install
12684 );
12685 }
12686
12687 return true;
12688 }
12689
12690 /**
12691 * Activate a given license on a collection of blogs/sites that are not yet opted-in.
12692 *
12693 * @author Vova Feldman (@svovaf)
12694 * @since 2.3.1
12695 *
12696 * @param \FS_User $user
12697 * @param string $license_key
12698 *
12699 * @return true|mixed True if successful, otherwise, the API result.
12700 */
12701 private function activate_license_on_site( FS_User $user, $license_key ) {
12702 return $this->activate_license_on_many_sites( $user, $license_key );
12703 }
12704
12705 /**
12706 * Activate a given license on a collection of blogs/sites that are not yet opted-in.
12707 *
12708 * @author Vova Feldman (@svovaf)
12709 * @since 2.0.0
12710 *
12711 * @param \FS_User $user
12712 * @param string $license_key
12713 * @param int[] $site_ids
12714 *
12715 * @return true|mixed True if successful, otherwise, the API result.
12716 */
12717 private function activate_license_on_many_sites(
12718 FS_User $user,
12719 $license_key,
12720 array $site_ids = array()
12721 ) {
12722 $sites = array();
12723 foreach ( $site_ids as $site_id ) {
12724 $sites[] = $this->get_site_info( array( 'blog_id' => $site_id ) );
12725 }
12726
12727 // Install the plugin.
12728 $result = $this->create_installs_with_user(
12729 $user,
12730 $license_key,
12731 false,
12732 $sites,
12733 false,
12734 true
12735 );
12736
12737 if ( ! $this->is_api_result_entity( $result ) &&
12738 ! $this->is_api_result_object( $result, 'installs' )
12739 ) {
12740 return $result;
12741 }
12742
12743 $installs = array();
12744
12745 if ( $this->is_api_result_entity( $result ) ) {
12746 $install = new FS_Site( $result );
12747
12748 $this->_user = $user;
12749
12750 $this->_store_site( true, null, $install );
12751
12752 $this->_site = $install;
12753
12754 $this->reset_anonymous_mode();
12755 } else {
12756 foreach ( $result->installs as $install ) {
12757 $installs[] = new FS_Site( $install );
12758 }
12759
12760 // Map site addresses to their blog IDs.
12761 $address_to_blog_map = $this->get_address_to_blog_map();
12762
12763 $first_blog_id = null;
12764
12765 foreach ( $installs as $install ) {
12766 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
12767 $blog_id = $address_to_blog_map[ $address ];
12768
12769 $this->_store_site( true, $blog_id, $install );
12770
12771 $this->reset_anonymous_mode( $blog_id );
12772
12773 if ( is_null( $first_blog_id ) ) {
12774 $first_blog_id = $blog_id;
12775 }
12776 }
12777
12778 if ( ! FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ) {
12779 $this->_storage->network_install_blog_id = $first_blog_id;
12780 }
12781 }
12782
12783 return true;
12784 }
12785
12786 /**
12787 * Sync site's license with user licenses.
12788 *
12789 * @author Vova Feldman (@svovaf)
12790 * @since 1.0.6
12791 *
12792 * @param FS_Plugin_License|null $new_license
12793 *
12794 * @return FS_Plugin_License|null
12795 */
12796 function _update_site_license( $new_license ) {
12797 $this->_logger->entrance();
12798
12799 /**
12800 * In case this call will be removed in the future, the `_sync_licenses()` method needs to be updated
12801 * accordingly so that it will also handle the case when an ownership change is done via license
12802 * activation.
12803 *
12804 * @author Leo Fajardo (@leorw)
12805 * @since 2.3.2
12806 */
12807 $this->set_license( $new_license );
12808
12809 if ( ! is_object( $new_license ) ) {
12810 $this->_site->license_id = null;
12811 $this->_sync_site_subscription( null );
12812
12813 return $this->_license;
12814 }
12815
12816 $this->_site->license_id = $this->_license->id;
12817
12818 if ( ! is_array( $this->_licenses ) ) {
12819 $this->_licenses = array();
12820 }
12821
12822 $is_license_found = false;
12823 for ( $i = 0, $len = count( $this->_licenses ); $i < $len; $i ++ ) {
12824 if ( $new_license->id == $this->_licenses[ $i ]->id ) {
12825 $this->_licenses[ $i ] = $new_license;
12826
12827 $is_license_found = true;
12828 break;
12829 }
12830 }
12831
12832 // If new license just append.
12833 if ( ! $is_license_found ) {
12834 $this->_licenses[] = $new_license;
12835 }
12836
12837 $this->_sync_site_subscription( $new_license );
12838
12839 return $this->_license;
12840 }
12841
12842 /**
12843 * @author Vova Feldman (@svovaf)
12844 * @since 2.3.1
12845 *
12846 * @param \FS_Plugin_License $license
12847 */
12848 private function set_license( FS_Plugin_License $license = null ) {
12849 $this->_license = $license;
12850
12851 $this->maybe_update_whitelabel_flag( $license );
12852 }
12853
12854 /**
12855 * @author Leo Fajardo (@leorw)
12856 * @since 2.3.1
12857 *
12858 * @param FS_Plugin_License $license
12859 */
12860 private function maybe_update_whitelabel_flag( $license ) {
12861 $is_whitelabeled = isset( $this->_storage->is_whitelabeled ) ?
12862 $this->_storage->is_whitelabeled :
12863 false;
12864
12865 if ( is_object( $license ) ) {
12866 $license_user = self::_get_user_by_id( $license->user_id );
12867
12868 if ( ! is_object( $license_user ) ) {
12869 // If foreign license, do not update the `is_whitelabeled` flag.
12870 return;
12871 }
12872
12873 if ( $this->is_addon() ) {
12874 /**
12875 * Store the last license data to the parent's storage since it's needed only when showing the
12876 * "Start Debug" dialog which is triggered from the "Account" page. This way, there's no need to
12877 * iterate over the add-ons just to get the last license data.
12878 */
12879 $this->get_parent_instance()->store_last_activated_license_data( $license, $license_user );
12880 } else {
12881 $this->store_last_activated_license_data( $license );
12882 }
12883
12884 if ( $license->is_whitelabeled ) {
12885 // Activated a developer license, data should be hidden.
12886 $is_whitelabeled = true;
12887 } else if ( $this->is_registered() && $this->_user->id == $license->user_id ) {
12888 // The account owner activated a regular license key, no need to hide the data.
12889 $is_whitelabeled = false;
12890 }
12891 }
12892
12893 $this->_storage->is_whitelabeled = $is_whitelabeled;
12894
12895 // Reset the whitelabeled status after update.
12896 $this->is_whitelabeled = null;
12897 if ( $this->is_addon() ) {
12898 $parent_fs = $this->get_parent_instance();
12899
12900 if ( is_object( $parent_fs ) ) {
12901 $parent_fs->is_whitelabeled = null;
12902 }
12903 }
12904 }
12905
12906 /**
12907 * @author Leo Fajardo (@leorw)
12908 * @since 2.3.1
12909 *
12910 * @param FS_Plugin_License $license
12911 * @param FS_User $license_user
12912 */
12913 private function store_last_activated_license_data( FS_Plugin_License $license, $license_user = null ) {
12914 if ( ! is_object( $license_user ) ) {
12915 $this->_storage->last_license_key = md5( $license->secret_key );
12916 $this->_storage->last_license_user_id = null;
12917 } else {
12918 $this->_storage->last_license_user_key = md5( $license_user->secret_key );
12919 $this->_storage->last_license_user_id = $license_user->id;
12920 }
12921 }
12922
12923 /**
12924 * @author Leo Fajardo (@leorw)
12925 * @since 2.3.1
12926 *
12927 * @param bool $ignore_data_debug_mode
12928 *
12929 * @return bool
12930 */
12931 function is_whitelabeled_by_flag( $ignore_data_debug_mode = false ) {
12932 if ( true !== $this->_storage->is_whitelabeled ) {
12933 return false;
12934 } else if ( $ignore_data_debug_mode ) {
12935 return true;
12936 }
12937
12938 $fs = $this->is_addon() ?
12939 $this->get_parent_instance() :
12940 $this;
12941
12942 return ! $fs->is_data_debug_mode();
12943 }
12944
12945 /**
12946 * @author Leo Fajardo (@leorw)
12947 * @since 2.3.1
12948 *
12949 * @return number
12950 */
12951 function get_last_license_user_id() {
12952 return ( FS_User::is_valid_id( $this->_storage->last_license_user_id ) ) ?
12953 $this->_storage->last_license_user_id :
12954 null;
12955 }
12956
12957 /**
12958 * @author Leo Fajardo (@leorw)
12959 * @since 2.3.1
12960 *
12961 * @param int $blog_id
12962 * @param bool $ignore_data_debug_mode
12963 *
12964 * @return bool
12965 */
12966 function is_whitelabeled( $ignore_data_debug_mode = false, $blog_id = null ) {
12967 if ( ! is_null( $blog_id ) ) {
12968 $this->switch_to_blog( $blog_id );
12969 }
12970
12971 if ( ! is_null( $this->is_whitelabeled ) ) {
12972 $is_whitelabeled = $this->is_whitelabeled;
12973 } else {
12974 $is_whitelabeled = false;
12975
12976 $is_whitelabeled_flag = $this->is_whitelabeled_by_flag( true );
12977
12978 if ( ! $this->has_addons() ) {
12979 $is_whitelabeled = $is_whitelabeled_flag;
12980 } else if ( $is_whitelabeled_flag ) {
12981 $is_whitelabeled = true;
12982 } else {
12983 if ( $this->is_registered() || $this->is_premium() ) {
12984 $addon_ids = $this->get_updated_account_addons();
12985 } else {
12986 $addons = self::get_all_addons();
12987
12988 $plugin_addons = isset( $addons[ $this->_plugin->id ] ) ?
12989 $addons[ $this->_plugin->id ] :
12990 array();
12991
12992 $addon_ids = array();
12993 foreach ( $plugin_addons as $addon ) {
12994 $addon_ids[] = $addon->id;
12995 }
12996 }
12997
12998 $installed_addons = $this->get_installed_addons();
12999 foreach ( $installed_addons as $fs_addon ) {
13000 $addon_ids[] = $fs_addon->get_id();
13001 }
13002
13003 if ( ! empty( $addon_ids ) ) {
13004 $addon_ids = array_unique( $addon_ids );
13005
13006 $is_network_level = (
13007 fs_is_network_admin() &&
13008 $this->is_network_active()
13009 );
13010
13011 foreach ( $addon_ids as $addon_id ) {
13012 $addon = $this->get_addon( $addon_id );
13013
13014 if ( ! is_object( $addon ) ) {
13015 continue;
13016 }
13017
13018 $addon_storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $addon->slug );
13019 $fs_addon = $this->is_addon_activated( $addon_id ) ?
13020 self::get_addon_instance( $addon_id ) :
13021 null;
13022
13023 $was_addon_network_activated = false;
13024
13025 if ( is_object( $fs_addon ) ) {
13026 $was_addon_network_activated = $fs_addon->is_network_active();
13027 } else if ( $is_network_level ) {
13028 $was_addon_network_activated = $addon_storage->get( 'was_plugin_loaded', false, true );
13029 }
13030
13031 $network_delegated_connection = (
13032 $was_addon_network_activated &&
13033 $addon_storage->get( 'is_delegated_connection', false, true )
13034 );
13035
13036 if (
13037 $is_network_level &&
13038 ( ! $was_addon_network_activated || $network_delegated_connection )
13039 ) {
13040 $sites = self::get_sites();
13041
13042 /**
13043 * If in network admin area and the add-on was not network-activated or network-activated
13044 * and network-delegated, find any add-on whose is_whitelabeled flag is true.
13045 */
13046 foreach ( $sites as $site ) {
13047 $site_info = $this->get_site_info( $site );
13048
13049 if ( $addon_storage->get( 'is_whitelabeled', false, $site_info['blog_id'] ) ) {
13050 $is_whitelabeled = true;
13051 break;
13052 }
13053 }
13054
13055 if ( $is_whitelabeled ) {
13056 break;
13057 }
13058 } else {
13059 /**
13060 * This will be executed when any of the following is met:
13061 * 1. Add-on was network-activated, not network-delegated, and in network admin area.
13062 * 2. Add-on was network-activated, network-delegated, and in site admin area.
13063 * 3. Add-on was not network-activated and in site admin area.
13064 */
13065 if ( true === $addon_storage->is_whitelabeled ) {
13066 $is_whitelabeled = true;
13067 break;
13068 }
13069 }
13070 }
13071 }
13072 }
13073
13074 $this->is_whitelabeled = $is_whitelabeled;
13075
13076 if ( ! $is_whitelabeled || ! $this->is_data_debug_mode() ) {
13077 $this->_admin_notices->remove_sticky( 'data_debug_mode_enabled' );
13078 }
13079
13080 if ( ! is_null( $blog_id ) ) {
13081 $this->restore_current_blog();
13082 }
13083 }
13084
13085 return (
13086 $is_whitelabeled &&
13087 ( $ignore_data_debug_mode || ! $this->is_data_debug_mode() )
13088 );
13089 }
13090
13091 /**
13092 * Sync site's subscription.
13093 *
13094 * @author Vova Feldman (@svovaf)
13095 * @since 1.0.9
13096 *
13097 * @param FS_Plugin_License|null $license
13098 *
13099 * @return bool|\FS_Subscription
13100 */
13101 private function _sync_site_subscription( $license ) {
13102 if ( ! is_object( $license ) ) {
13103 $this->delete_unused_subscriptions();
13104
13105 return false;
13106 }
13107
13108 // Load subscription details if not lifetime.
13109 $subscription = $license->is_lifetime() ?
13110 false :
13111 $this->_fetch_site_license_subscription();
13112
13113 if ( is_object( $subscription ) && ! isset( $subscription->error ) ) {
13114 $this->store_subscription( $subscription );
13115 } else {
13116 $this->delete_unused_subscriptions();
13117 }
13118
13119 return $subscription;
13120 }
13121
13122 /**
13123 * @author Vova Feldman (@svovaf)
13124 * @since 1.0.6
13125 *
13126 * @return bool|\FS_Plugin_License
13127 */
13128 function _get_license() {
13129 if ( ! fs_is_network_admin() || is_object( $this->_license ) ) {
13130 return $this->_license;
13131 }
13132
13133 return $this->_get_available_premium_license();
13134 }
13135
13136 /**
13137 * @param number $license_id
13138 *
13139 * @return null|\FS_Subscription
13140 */
13141 function _get_subscription( $license_id ) {
13142 if ( ! isset( $this->_storage->subscriptions ) ||
13143 empty( $this->_storage->subscriptions )
13144 ) {
13145 return null;
13146 }
13147
13148 foreach ( fs_get_entities( $this->_storage->subscriptions, FS_Subscription::get_class_name() ) as $subscription ) {
13149 if ( $subscription->license_id == $license_id ) {
13150 return $subscription;
13151 }
13152 }
13153
13154 return null;
13155 }
13156
13157 /**
13158 * @author Leo Fajardo (@leorw)
13159 * @since 2.0.0
13160 *
13161 * @param FS_Subscription $subscription
13162 */
13163 function store_subscription( FS_Subscription $subscription ) {
13164 if ( ! isset( $this->_storage->subscriptions ) ) {
13165 $this->_storage->subscriptions = array();
13166 }
13167
13168 if ( empty( $this->_storage->subscriptions ) || ! is_multisite() ) {
13169 $this->_storage->subscriptions = array( $subscription );
13170
13171 return;
13172 }
13173
13174 $subscriptions = fs_get_entities( $this->_storage->subscriptions, FS_Subscription::get_class_name() );
13175
13176 $updated_subscription = false;
13177 foreach ( $subscriptions as $key => $existing_subscription ) {
13178 if ( $existing_subscription->id == $subscription->id ) {
13179 $subscriptions[ $key ] = $subscription;
13180 $updated_subscription = true;
13181 break;
13182 }
13183 }
13184
13185 if ( ! $updated_subscription ) {
13186 $subscriptions[] = $subscription;
13187 }
13188
13189 $this->_storage->subscriptions = $subscriptions;
13190 }
13191
13192 /**
13193 * @author Leo Fajardo (@leorw)
13194 * @since 2.0.0
13195 */
13196 function delete_unused_subscriptions() {
13197 if ( ! isset( $this->_storage->subscriptions ) ||
13198 empty( $this->_storage->subscriptions ) ||
13199 // Clean up only if there are already at least 3 subscriptions.
13200 ( count( $this->_storage->subscriptions ) < 3 )
13201 ) {
13202 return;
13203 }
13204
13205 if ( ! is_multisite() ) {
13206 // If not multisite, there should only be 1 subscription, so just clear the array.
13207 $this->_storage->subscriptions = array();
13208
13209 return;
13210 }
13211
13212 $subscriptions_to_keep_by_license_id_map = array();
13213 $sites = self::get_sites();
13214 foreach ( $sites as $site ) {
13215 $blog_id = self::get_site_blog_id( $site );
13216 $install = $this->get_install_by_blog_id( $blog_id );
13217
13218 if ( ! is_object( $install ) ||
13219 ! FS_Plugin_License::is_valid_id( $install->license_id )
13220 ) {
13221 continue;
13222 }
13223
13224 $subscriptions_to_keep_by_license_id_map[ $install->license_id ] = true;
13225 }
13226
13227 if ( empty( $subscriptions_to_keep_by_license_id_map ) ) {
13228 $this->_storage->subscriptions = array();
13229
13230 return;
13231 }
13232
13233 foreach ( $this->_storage->subscriptions as $key => $subscription ) {
13234 if ( ! isset( $subscriptions_to_keep_by_license_id_map[ $subscription->license_id ] ) ) {
13235 unset( $this->_storage->subscriptions[ $key ] );
13236 }
13237 }
13238 }
13239
13240 /**
13241 * @author Vova Feldman (@svovaf)
13242 * @since 1.0.2
13243 *
13244 * @param string $plan Plan name
13245 * @param bool $exact If true, looks for exact plan. If false, also check "higher" plans.
13246 *
13247 * @return bool
13248 */
13249 function is_plan( $plan, $exact = false ) {
13250 $this->_logger->entrance();
13251
13252 if ( ! $this->is_registered() ) {
13253 return false;
13254 }
13255
13256 $plan = strtolower( $plan );
13257
13258 $current_plan_name = $this->get_plan_name();
13259
13260 if ( $current_plan_name === $plan ) {
13261 // Exact plan.
13262 return true;
13263 } else if ( $exact ) {
13264 // Required exact, but plans are different.
13265 return false;
13266 }
13267
13268 $current_plan_order = - 1;
13269 $required_plan_order = PHP_INT_MAX;
13270 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
13271 if ( $plan === $this->_plans[ $i ]->name ) {
13272 $required_plan_order = $i;
13273 } else if ( $current_plan_name === $this->_plans[ $i ]->name ) {
13274 $current_plan_order = $i;
13275 }
13276 }
13277
13278 return ( $current_plan_order > $required_plan_order );
13279 }
13280
13281 /**
13282 * Check if module has only one plan.
13283 *
13284 * @author Vova Feldman (@svovaf)
13285 * @since 1.2.1.7
13286 *
13287 * @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.
13288 *
13289 * @return bool
13290 */
13291 function is_single_plan( $double_check = false ) {
13292 $this->_logger->entrance();
13293
13294 if ( ! $this->is_registered() ||
13295 ! is_array( $this->_plans ) ||
13296 0 === count( $this->_plans )
13297 ) {
13298 return true;
13299 }
13300
13301 $has_free_plan = $this->has_free_plan();
13302
13303 if ( ! $has_free_plan && $double_check ) {
13304 foreach ( $this->_plans as $plan ) {
13305 if ( $plan->is_free() ) {
13306 $has_free_plan = true;
13307 break;
13308 }
13309 }
13310 }
13311
13312 return ( 1 === ( count( $this->_plans ) - ( $has_free_plan ? 1 : 0 ) ) );
13313 }
13314
13315 /**
13316 * Check if plan based on trial. If not in trial mode, should return false.
13317 *
13318 * @since 1.0.9
13319 *
13320 * @param string $plan Plan name
13321 * @param bool $exact If true, looks for exact plan. If false, also check "higher" plans.
13322 *
13323 * @return bool
13324 */
13325 function is_trial_plan( $plan, $exact = false ) {
13326 $this->_logger->entrance();
13327
13328 if ( ! $this->is_registered() ) {
13329 return false;
13330 }
13331
13332 if ( ! $this->is_trial() ) {
13333 return false;
13334 }
13335
13336 $trial_plan = $this->get_trial_plan();
13337
13338 if ( $trial_plan->name === $plan ) {
13339 // Exact plan.
13340 return true;
13341 } else if ( $exact ) {
13342 // Required exact, but plans are different.
13343 return false;
13344 }
13345
13346 $current_plan_order = - 1;
13347 $required_plan_order = - 1;
13348 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
13349 if ( $plan === $this->_plans[ $i ]->name ) {
13350 $required_plan_order = $i;
13351 } else if ( $trial_plan->name === $this->_plans[ $i ]->name ) {
13352 $current_plan_order = $i;
13353 }
13354 }
13355
13356 return ( $current_plan_order > $required_plan_order );
13357 }
13358
13359 /**
13360 * Check if plugin has any paid plans.
13361 *
13362 * @author Vova Feldman (@svovaf)
13363 * @since 1.0.7
13364 *
13365 * @return bool
13366 */
13367 function has_paid_plan() {
13368 return $this->_has_paid_plans ||
13369 FS_Plan_Manager::instance()->has_paid_plan( $this->_plans );
13370 }
13371
13372 /**
13373 * Check if plugin has any plan with a trail.
13374 *
13375 * @author Vova Feldman (@svovaf)
13376 * @since 1.0.9
13377 *
13378 * @return bool
13379 */
13380 function has_trial_plan() {
13381 /**
13382 * @author Vova Feldman(@svovaf)
13383 * @since 1.2.1.5
13384 *
13385 * Allow setting a trial from the SDK without calling the API.
13386 * But, if the user did opt-in, continue using the real data from the API.
13387 */
13388 if ( $this->_trial_days >= 0 ) {
13389 return true;
13390 }
13391
13392 return $this->_storage->get( 'has_trial_plan', false );
13393 }
13394
13395 /**
13396 * Check if plugin has any free plan, or is it premium only.
13397 *
13398 * Note: If no plans configured, assume plugin is free.
13399 *
13400 * @author Vova Feldman (@svovaf)
13401 * @since 1.0.7
13402 *
13403 * @return bool
13404 */
13405 function has_free_plan() {
13406 return ! $this->is_only_premium();
13407 }
13408
13409 /**
13410 * Displays a license activation dialog box when the user clicks on the "Activate License"
13411 * or "Change License" link on the plugins
13412 * page.
13413 *
13414 * @author Leo Fajardo (@leorw)
13415 * @since 1.1.9
13416 */
13417 function _add_license_activation_dialog_box() {
13418 $vars = array(
13419 'id' => $this->_module_id,
13420 );
13421
13422 fs_require_template( 'forms/license-activation.php', $vars );
13423 fs_require_template( 'forms/resend-key.php', $vars );
13424 }
13425
13426 /**
13427 * Displays an email address update dialog box when the user clicks on the email address "Edit" button on the "Account" page.
13428 *
13429 * @author Leo Fajardo (@leorw)
13430 * @since 2.5.0
13431 */
13432 function _add_email_address_update_dialog_box() {
13433 $vars = array( 'id' => $this->_module_id );
13434
13435 fs_require_template( 'forms/email-address-update.php', $vars );
13436 }
13437
13438 /**
13439 * @author Leo Fajardo (@leorw)
13440 * @since 2.5.0
13441 */
13442 function _add_email_address_update_option() {
13443 if ( ! $this->should_handle_user_change() ) {
13444 return;
13445 }
13446
13447 // Add email address update AJAX handler.
13448 $this->add_ajax_action( 'update_email_address', array( &$this, '_email_address_update_ajax_handler' ) );
13449 }
13450
13451 /**
13452 * @author Leo Fajardo (@leorw)
13453 * @since 2.5.0
13454 */
13455 function _email_address_update_ajax_handler() {
13456 $this->check_ajax_referer( 'update_email_address' );
13457
13458 $new_email_address = fs_request_get( 'email_address' );
13459 $transfer_type = fs_request_get( 'transfer_type' );
13460
13461 $result = $this->update_email( $new_email_address );
13462
13463 if ( ! FS_Api::is_api_error( $result ) ) {
13464 self::shoot_ajax_success();
13465 }
13466
13467 $error = '';
13468
13469 if ( FS_Api::is_api_error_object( $result ) ) {
13470 switch ( $result->error->code ) {
13471 case 'user_exist':
13472 case 'account_verification_required':
13473 $error = array(
13474 'code' => 'change_ownership',
13475 'url' => $this->get_account_url( 'change_owner', array(
13476 'state' => 'init',
13477 'candidate_email' => $new_email_address,
13478 'transfer_type' => $transfer_type,
13479 ) ),
13480 );
13481
13482 break;
13483 }
13484 }
13485
13486 if ( empty( $error ) ) {
13487 $error = is_object( $result ) ?
13488 var_export( $result->error, true ) :
13489 $result;
13490 }
13491
13492 self::shoot_ajax_failure( $error );
13493 }
13494
13495 /**
13496 * Returns a collection of IDs of installs that are associated with the context product and its add-ons, and activated with foreign licenses.
13497 *
13498 * @author Leo Fajardo (@leorw)
13499 * @since 2.3.2
13500 *
13501 * @return number[]
13502 */
13503 function get_installs_ids_with_foreign_licenses() {
13504 $installs = array();
13505
13506 if (
13507 is_object( $this->_license ) &&
13508 $this->_site->user_id != $this->_license->user_id
13509 ) {
13510 $installs[] = $this->_site->id;
13511 }
13512
13513 /**
13514 * Also try to get foreign licenses for the context product's add-ons.
13515 */
13516 $installs_by_slug_map = $this->get_parent_and_addons_installs_info();
13517
13518 foreach ( $installs_by_slug_map as $slug => $install_info ) {
13519 if ( $slug == $this->get_slug() ) {
13520 continue;
13521 }
13522
13523 $install = $install_info['install'];
13524 $license = $install_info['license'];
13525
13526 if (
13527 is_object( $license ) &&
13528 $install->user_id != $license->user_id
13529 ) {
13530 $installs[] = $install->id;
13531 }
13532 }
13533
13534 return $installs;
13535 }
13536
13537 /**
13538 * Displays the "Change User" dialog box when the user clicks on the "Change User" button on the "Account" page.
13539 *
13540 * @author Leo Fajardo (@leorw)
13541 * @since 2.3.2
13542 *
13543 * @param number[] $install_ids
13544 */
13545 function _add_user_change_dialog_box( $install_ids ) {
13546 $vars = array(
13547 'id' => $this->_module_id,
13548 'license_owners' => $this->fetch_installs_licenses_owners_data( $install_ids )
13549 );
13550
13551 fs_require_template( 'forms/user-change.php', $vars );
13552 }
13553
13554 /**
13555 * @author Leo Fajardo (@leorw)
13556 * @since 2.3.1
13557 */
13558 function _add_data_debug_mode_dialog_box() {
13559 $vars = array(
13560 'id' => $this->_module_id,
13561 );
13562
13563 fs_require_template( 'forms/data-debug-mode.php', $vars );
13564 }
13565
13566 /**
13567 * Displays a subscription cancellation dialog box when the user clicks on the "Deactivate License"
13568 * link on the "Account" page or deactivates a plugin and there's an active subscription that is
13569 * either associated with a non-lifetime single-site license or non-lifetime multisite license that
13570 * is only activated on a single production site.
13571 *
13572 * @author Leo Fajardo (@leorw)
13573 * @since 2.2.1
13574 *
13575 * @param bool $is_license_deactivation
13576 *
13577 * @return array
13578 */
13579 function _get_subscription_cancellation_dialog_box_template_params( $is_license_deactivation = false ) {
13580 if ( fs_is_network_admin() ) {
13581 // Subscription cancellation dialog box is currently not supported for multisite networks.
13582 return array();
13583 }
13584
13585 if ( $this->is_whitelabeled() ) {
13586 return array();
13587 }
13588
13589 $license = $this->_get_license();
13590
13591 /**
13592 * 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.
13593 *
13594 * @author Leo Fajardo (@leorw) (Comment added by Vova Feldman @svovaf)
13595 * @since 2.2.1
13596 */
13597 if ( ! is_object( $license ) ||
13598 $license->is_lifetime() ||
13599 ( ! $license->is_single_site() && $license->activated > 1 )
13600 ) {
13601 return array();
13602 }
13603
13604 /**
13605 * @var FS_Subscription $subscription
13606 */
13607 $subscription = $this->_get_subscription( $license->id );
13608 if ( ! is_object( $subscription ) || ! $subscription->is_active() ) {
13609 return array();
13610 }
13611
13612 return array(
13613 'id' => $this->_module_id,
13614 'license' => $license,
13615 'has_trial' => $this->is_paid_trial(),
13616 'is_license_deactivation' => $is_license_deactivation,
13617 );
13618 }
13619
13620 /**
13621 * @author Leo Fajardo (@leorw)
13622 * @since 2.0.2
13623 */
13624 function _add_premium_version_upgrade_selection_dialog_box() {
13625 $modules_update = get_site_transient( $this->is_theme() ? 'update_themes' : 'update_plugins' );
13626 if ( ! isset( $modules_update->response[ $this->_plugin_basename ] ) ) {
13627 return;
13628 }
13629
13630 $vars = array(
13631 'id' => $this->_module_id,
13632 'new_version' => is_object( $modules_update->response[ $this->_plugin_basename ] ) ?
13633 $modules_update->response[ $this->_plugin_basename ]->new_version :
13634 $modules_update->response[ $this->_plugin_basename ]['new_version']
13635 );
13636
13637 fs_require_template( 'forms/premium-versions-upgrade-metadata.php', $vars );
13638 fs_require_once_template( 'forms/premium-versions-upgrade-handler.php', $vars );
13639 }
13640
13641 /**
13642 * Displays the opt-out dialog box when the user clicks on the "Opt Out" link on the "Plugins"
13643 * page.
13644 *
13645 * @author Leo Fajardo (@leorw)
13646 * @since 1.2.1.5
13647 */
13648 function _add_optout_dialog() {
13649 if ( $this->is_theme() ) {
13650 $vars = null;
13651 fs_require_once_template( '/js/jquery.content-change.php', $vars );
13652 }
13653
13654 $vars = array( 'id' => $this->_module_id );
13655 fs_require_template( 'forms/optout.php', $vars );
13656 }
13657
13658 /**
13659 * Prepare page to include all required UI and logic for the license activation dialog.
13660 *
13661 * @author Vova Feldman (@svovaf)
13662 * @since 1.2.0
13663 */
13664 function _add_license_activation() {
13665 if ( $this->is_migration() ) {
13666 return;
13667 }
13668
13669 if ( ! $this->is_user_admin() ) {
13670 // Only admins can activate a license.
13671 return;
13672 }
13673
13674 if ( ! $this->has_paid_plan() ) {
13675 // Module doesn't have any paid plans.
13676 return;
13677 }
13678
13679 if (
13680 $this->has_premium_version() &&
13681 ! $this->is_premium() &&
13682 /**
13683 * Also handle the case when an upgrade was made using the free version.
13684 *
13685 * @author Leo Fajardo (@leorw)
13686 * @since 2.3.2
13687 */
13688 ! is_object( $this->_get_license() )
13689 ) {
13690 // Only add license activation logic to the premium version, or in case of a serviceware plugin, also in the free version.
13691 return;
13692 }
13693
13694 // Add license activation link and AJAX request handler.
13695 if ( self::is_plugins_page() ) {
13696 $is_network_admin = fs_is_network_admin();
13697
13698 if (
13699 ( $is_network_admin && $this->is_network_active() && ! $this->is_network_delegated_connection() ) ||
13700 ( ! $is_network_admin && ( ! $this->is_network_active() || $this->is_delegated_connection() ) )
13701 ) {
13702 if (
13703 $this->is_premium() ||
13704 ( $this->has_paid_plan() && ! $this->has_premium_version() )
13705 ) {
13706 /**
13707 * @since 1.2.0 Add license action link only on plugins page.
13708 */
13709 $this->_add_license_action_link();
13710 }
13711 }
13712 }
13713
13714 // Add license activation AJAX callback.
13715 $this->add_ajax_action( 'activate_license', array( &$this, '_activate_license_ajax_action' ) );
13716
13717 // Add resend license AJAX callback.
13718 $this->add_ajax_action( 'resend_license_key', array( &$this, '_resend_license_key_ajax_action' ) );
13719 }
13720
13721 /**
13722 * Prepares page to include all required UI and logic for the "Change User" dialog.
13723 *
13724 * @author Leo Fajardo (@leorw)
13725 * @since 2.3.2
13726 */
13727 function _add_user_change_option() {
13728 if ( ! $this->should_handle_user_change() ) {
13729 return;
13730 }
13731
13732 $installs_ids_with_foreign_licenses = $this->get_installs_ids_with_foreign_licenses();
13733
13734 if ( empty( $installs_ids_with_foreign_licenses ) ) {
13735 // Handle user change only when the parent product or one of its add-ons is activated with a foreign license.
13736 return;
13737 }
13738
13739 // Add user change AJAX handler.
13740 $this->add_ajax_action( 'change_user', array( &$this, '_user_change_ajax_action' ) );
13741 }
13742
13743 /**
13744 * @author Leo Fajardo (@leorw)
13745 * @since 2.3.2
13746 */
13747 function should_handle_user_change() {
13748 if ( ! $this->is_user_admin() ) {
13749 // Only admins can change user.
13750 return false;
13751 }
13752
13753 if ( $this->is_addon() ) {
13754 return false;
13755 }
13756
13757 if ( ! $this->is_registered() ) {
13758 return false;
13759 }
13760
13761 if (
13762 $this->is_network_active() &&
13763 ( fs_is_network_admin() || ! $this->is_site_delegated_connection() )
13764 ) {
13765 // Handle only on site-level "Account" section for now.
13766 return false;
13767 }
13768
13769 return true;
13770 }
13771
13772 /**
13773 * @author Leo Fajardo (@leorw)
13774 * @since 2.0.2
13775 */
13776 function _add_premium_version_upgrade_selection() {
13777 if ( ! $this->is_user_admin() ) {
13778 return;
13779 }
13780
13781 if ( ! $this->is_premium() || $this->has_any_active_valid_license() ) {
13782 // This is relevant only to the free versions and premium versions without an active license.
13783 return;
13784 }
13785
13786 if ( self::is_updates_page() || ( $this->is_plugin() && self::is_plugins_page() ) ) {
13787 $this->_add_premium_version_upgrade_selection_action();
13788 }
13789 }
13790
13791 /**
13792 * @author Edgar Melkonyan
13793 * @since 2.4.1
13794 *
13795 * @throws Freemius_Exception
13796 */
13797 function _toggle_whitelabel_mode_ajax_handler() {
13798 $this->_logger->entrance();
13799
13800 $this->check_ajax_referer( 'toggle_whitelabel_mode' );
13801
13802 if ( ! $this->is_user_admin() ) {
13803 // Only for admins.
13804 self::shoot_ajax_failure();
13805 }
13806
13807 $license = $this->get_api_user_scope()->call(
13808 "/licenses/{$this->_site->license_id}.json",
13809 'put',
13810 array( 'is_whitelabeled' => ! $this->_license->is_whitelabeled )
13811 );
13812
13813 if ( ! $this->is_api_result_entity( $license ) ) {
13814 self::shoot_ajax_failure(
13815 FS_Api::is_api_error_object( $license ) ?
13816 $license->error->message :
13817 fs_text_inline( "An unknown error has occurred while trying to toggle the license's white-label mode.", 'unknown-error-occurred', $this->get_slug() )
13818 );
13819 }
13820
13821 $this->_license->is_whitelabeled = $license->is_whitelabeled;
13822 $this->_store_licenses();
13823
13824 $this->_sync_license();
13825
13826 if ( ! $license->is_whitelabeled ) {
13827 $this->_admin_notices->remove_sticky( 'license_whitelabeled' );
13828 } else {
13829 $this->_admin_notices->add_sticky(
13830 sprintf(
13831 $this->get_text_inline(
13832 '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.',
13833 'license_whitelabeled'
13834 ),
13835 "<strong>{$this->get_plugin_title()}</strong>",
13836 sprintf( '<a href="https://users.freemius.com" target="_blank">%s</a>', $this->get_text_inline( 'User Dashboard', 'user-dashboard' ) ),
13837 sprintf( '<a href="#" class="fs-toggle-whitelabel-mode">%s</a>', $this->get_text_inline( 'revert it now', 'revert-it-now' ) )
13838 ),
13839 'license_whitelabeled'
13840 );
13841 }
13842
13843 self::shoot_ajax_response( array( 'success' => true ) );
13844 }
13845
13846 /**
13847 * @author Leo Fajardo (@leorw)
13848 * @since 2.3.0
13849 */
13850 function _add_beta_mode_update_handler() {
13851 if ( ! $this->is_user_admin() ) {
13852 return;
13853 }
13854
13855 if ( ! $this->is_premium() ) {
13856 return;
13857 }
13858
13859 $this->add_ajax_action( 'set_beta_mode', array( &$this, '_set_beta_mode_ajax_handler' ) );
13860 }
13861
13862 /**
13863 * @author Leo Fajardo (@leorw)
13864 * @since 2.3.0
13865 */
13866 function _set_beta_mode_ajax_handler() {
13867 $this->_logger->entrance();
13868
13869 $this->check_ajax_referer( 'set_beta_mode' );
13870
13871 if ( ! $this->is_user_admin() ) {
13872 // Only for admins.
13873 self::shoot_ajax_failure();
13874 }
13875
13876 $is_beta = trim( fs_request_get( 'is_beta', '', 'post' ) );
13877
13878 if ( empty( $is_beta ) || ! in_array( $is_beta, array( 'true', 'false' ) ) ) {
13879 self::shoot_ajax_failure();
13880 }
13881
13882 $site = $this->api_site_call(
13883 '',
13884 'put',
13885 array(
13886 'is_beta' => ( 'true' == $is_beta ),
13887 'fields' => 'is_beta'
13888 )
13889 );
13890
13891 if ( ! $this->is_api_result_entity( $site ) ) {
13892 self::shoot_ajax_failure(
13893 FS_Api::is_api_error_object( $site ) ?
13894 $site->error->message :
13895 fs_text_inline( "An unknown error has occurred while trying to set the user's beta mode.", 'unknown-error-occurred', $this->get_slug() )
13896 );
13897 }
13898
13899 $this->_site->is_beta = $site->is_beta;
13900 $this->_store_site();
13901
13902 self::shoot_ajax_response( array( 'success' => true ) );
13903 }
13904
13905 /**
13906 * License activation WP AJAX handler.
13907 *
13908 * @author Leo Fajardo (@leorw)
13909 * @since 1.1.9
13910 *
13911 * @uses Freemius::activate_license()
13912 */
13913 function _activate_license_ajax_action() {
13914 $this->_logger->entrance();
13915
13916 $this->check_ajax_referer( 'activate_license' );
13917
13918 $license_key = trim( fs_request_get( 'license_key' ) );
13919
13920 if ( empty( $license_key ) ) {
13921 exit;
13922 }
13923
13924 $sites = fs_is_network_admin() ?
13925 fs_request_get( 'sites', array(), 'post' ) :
13926 array();
13927
13928 $result = $this->activate_license(
13929 $license_key,
13930 $sites,
13931 fs_request_get_bool( 'is_marketing_allowed', null ),
13932 fs_request_get( 'blog_id', null ),
13933 fs_request_get( 'module_id', null, 'post' ),
13934 fs_request_get( 'user_id', null ),
13935 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
13936 fs_request_get_bool( 'is_diagnostic_tracking_allowed', null )
13937 );
13938
13939 if (
13940 $result['success'] &&
13941 $this->is_bundle_license_auto_activation_enabled()
13942 ) {
13943 $license = new FS_Plugin_License();
13944 $license->secret_key = $license_key;
13945
13946 $this->maybe_activate_bundle_license( $license, $sites );
13947 }
13948
13949 echo json_encode( $result );
13950
13951 exit;
13952 }
13953
13954 /**
13955 * User change WP AJAX handler.
13956 *
13957 * @author Leo Fajardo (@leorw)
13958 * @since 2.3.2
13959 */
13960 function _user_change_ajax_action() {
13961 $this->_logger->entrance();
13962
13963 $this->check_ajax_referer( 'change_user' );
13964
13965 $new_email_address = trim( fs_request_get( 'email_address', '' ) );
13966 $new_user_id = fs_request_get( 'user_id' );
13967
13968 if ( empty( $new_email_address ) && ! FS_User::is_valid_id( $new_user_id ) ) {
13969 self::shoot_ajax_failure( fs_text_inline( 'Invalid new user ID or email address.', 'invalid-new-user-id-or-email', $this->get_slug() ) );
13970 }
13971
13972 $params = array();
13973
13974 if ( ! empty( $new_email_address ) ) {
13975 $params['user_email'] = $new_email_address;
13976 } else {
13977 $params['user_id'] = $new_user_id;
13978 }
13979
13980 $installs_info_by_slug_map = $this->get_parent_and_addons_installs_info();
13981 $install_ids = array();
13982
13983 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
13984 $install_ids[ $slug ] = $install_info['install']->id;
13985 }
13986
13987 $params['install_ids'] = implode( ',', array_values( $install_ids ) );
13988
13989 $install = $this->get_api_site_scope()->call( $this->add_show_pending( '/' ), 'put', $params );
13990
13991 if ( FS_Api::is_api_error( $install ) ) {
13992 $error = '';
13993
13994 if ( is_object( $install ) ) {
13995 switch ( $install->error->code ) {
13996 case 'user_exist':
13997 $error = (
13998 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...' .
13999 $this->get_text_inline( 'Sorry, we could not complete the email update. Another user with the same email is already registered.', 'user-exist-message' ) . ' ' .
14000 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>' ) .
14001 sprintf(
14002 '<a style="line-height: 40px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
14003 $this->get_account_url( 'change_owner', array(
14004 'state' => 'init',
14005 'candidate_email' => $new_email_address
14006 ) ),
14007 $this->get_text_inline( 'Change Ownership', 'change-ownership' )
14008 )
14009 );
14010 break;
14011 }
14012 }
14013
14014 if ( empty( $error ) ) {
14015 $error = FS_Api::is_api_error_object( $install ) ?
14016 $install->error->message :
14017 var_export( $install->error, true );
14018 }
14019
14020 self::shoot_ajax_failure( $error );
14021 } else {
14022 if (
14023 // If successful ownership change.
14024 $this->get_user()->id != $install->user_id ||
14025 ! empty( $new_email_address )
14026 ) {
14027 $this->complete_ownership_change_by_license( $install->user_id, $install_ids );
14028 }
14029 }
14030
14031 self::shoot_ajax_success();
14032 }
14033
14034 /**
14035 * @author Leo Fajardo (@leorw)
14036 * @since 2.3.2.14
14037 */
14038 function starting_migration() {
14039 if ( ! empty( $this->_storage->license_migration ) ) {
14040 // Do not overwrite the data if already set.
14041 return;
14042 }
14043
14044 $this->_storage->license_migration = array(
14045 'is_migrating' => true,
14046 'start_timestamp' => time()
14047 );
14048 }
14049
14050 /**
14051 * @author Leo Fajardo (@leorw)
14052 * @since 2.3.2.14
14053 */
14054 function is_migration() {
14055 if ( $this->is_addon() ) {
14056 return $this->get_parent_instance()->is_migration();
14057 }
14058
14059 if ( empty( $this->_storage->license_migration ) ) {
14060 return false;
14061 }
14062
14063 if ( ! $this->_storage->license_migration['is_migrating'] ) {
14064 return false;
14065 }
14066
14067 return (
14068 // Return `true` if the migration is within 5 minutes from the starting time.
14069 ( time() - $this->_storage->license_migration['start_timestamp'] ) <= WP_FS__TIME_5_MIN_IN_SEC
14070 );
14071 }
14072
14073 /**
14074 *
14075 * A helper method to activate migrated licenses. If the product is network activated and integrated, the method will network activate the license.
14076 *
14077 * @author Vova Feldman (@svovaf)
14078 * @since 2.3.0
14079 *
14080 * @param string $license_key
14081 * @param null|bool $is_marketing_allowed
14082 * @param null|number $plugin_id
14083 * @param array $sites
14084 * @param int $blog_id
14085 *
14086 * @return array {
14087 * @var bool $success
14088 * @var string $error
14089 * @var string $next_page
14090 * }
14091 *
14092 * @uses Freemius::activate_license()
14093 */
14094 function activate_migrated_license(
14095 $license_key,
14096 $is_marketing_allowed = null,
14097 $plugin_id = null,
14098 $sites = array(),
14099 $blog_id = null
14100 ) {
14101 $this->_logger->entrance();
14102
14103 $result = $this->activate_license(
14104 $license_key,
14105 ( empty( $sites ) && is_null( $blog_id ) && $this->is_network_active() ) ?
14106 $this->get_sites_for_network_level_optin() :
14107 $sites,
14108 $is_marketing_allowed,
14109 $blog_id,
14110 $plugin_id
14111 );
14112
14113 // No need to show the sticky after license activation notice after migrating a license.
14114 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
14115
14116 return $result;
14117 }
14118
14119 /**
14120 * @author Leo Fajardo (@leorw)
14121 * @since 2.3.1
14122 *
14123 * @return string
14124 */
14125 function get_pricing_js_path() {
14126 if ( ! isset( $this->_pricing_js_path ) ) {
14127 $pricing_js_path = $this->apply_filters( 'freemius_pricing_js_path', '' );
14128
14129 if ( empty( $pricing_js_path ) ) {
14130 global $fs_active_plugins;
14131
14132 foreach ( $fs_active_plugins->plugins as $sdk_path => $data ) {
14133 if ( $data->plugin_path == $this->get_plugin_basename() ) {
14134 $plugin_or_theme_root_dir = ( $this->is_plugin() ? WP_PLUGIN_DIR : get_theme_root( get_stylesheet() ) );
14135
14136 $pricing_js_path = $plugin_or_theme_root_dir
14137 . '/'
14138 // The basename will be `plugins`, `themes`, or the basename of a custom plugins or themes directory.
14139 . str_replace( '../' . basename( $plugin_or_theme_root_dir ) . '/', '', $sdk_path )
14140 . '/includes/freemius-pricing/freemius-pricing.js';
14141
14142 break;
14143 }
14144 }
14145 }
14146
14147 $this->_pricing_js_path = $pricing_js_path;
14148 }
14149
14150 return $this->_pricing_js_path;
14151 }
14152
14153 /**
14154 * @author Leo Fajardo (@leorw)
14155 * @since 2.3.1
14156 *
14157 * @return bool
14158 */
14159 function should_use_external_pricing() {
14160 if ( is_null( $this->_use_external_pricing ) ) {
14161 $pricing_js_path = $this->get_pricing_js_path();
14162
14163 $this->_use_external_pricing = ( empty( $pricing_js_path ) || ! file_exists( $pricing_js_path ) );
14164 }
14165
14166 return $this->_use_external_pricing;
14167 }
14168
14169 /**
14170 * The implementation of this method was previously in `_activate_license_ajax_action()`.
14171 *
14172 * @author Vova Feldman (@svovaf)
14173 * @since 2.2.4
14174 * @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).
14175 * @param string $license_key
14176 * @param array $sites
14177 * @param null|bool $is_marketing_allowed
14178 * @param null|int $blog_id
14179 * @param null|number $plugin_id
14180 * @param null|number $license_owner_id
14181 * @param bool|null $is_extensions_tracking_allowed
14182 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2 to allow license activation with minimal data footprint.
14183 *
14184 *
14185 * @return array {
14186 * @var bool $success
14187 * @var string $error
14188 * @var string $next_page
14189 * }
14190 */
14191 private function activate_license(
14192 $license_key,
14193 $sites = array(),
14194 $is_marketing_allowed = null,
14195 $blog_id = null,
14196 $plugin_id = null,
14197 $license_owner_id = null,
14198 $is_extensions_tracking_allowed = null,
14199 $is_diagnostic_tracking_allowed = null
14200 ) {
14201 $this->_logger->entrance();
14202
14203 $license_key = trim( $license_key );
14204
14205 $is_network_activation_or_migration = (
14206 fs_is_network_admin() ||
14207 ( ! empty( $sites ) && $this->is_migration() )
14208 );
14209
14210 if ( ! $is_network_activation_or_migration ) {
14211 // If the license activation is executed outside the context of a network admin, ignore the sites collection.
14212 $sites = array();
14213 }
14214
14215 $fs = ( empty($plugin_id) || $plugin_id == $this->_module_id ) ?
14216 $this :
14217 $this->get_addon_instance( $plugin_id );
14218
14219 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
14220 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
14221 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
14222 ) );
14223
14224 $error = false;
14225 $next_page = false;
14226
14227 $has_valid_blog_id = is_numeric( $blog_id );
14228
14229 $user = null;
14230
14231 if ( $fs->is_addon() && $fs->get_parent_instance()->is_registered() ) {
14232 /**
14233 * When activating an add-on's license and the parent is opted-in, activate the license with the parent's opted-in user context.
14234 *
14235 * @author Vova Feldman (@svovaf)
14236 */
14237 $user = $fs->get_parent_instance()->get_current_or_network_user();
14238 } else if ( $fs->is_registered() ) {
14239 $user = $fs->get_current_or_network_user();
14240 }
14241
14242 if ( $has_valid_blog_id ) {
14243 /**
14244 * If a specific blog ID was provided, activate the license only on the specific blog that is associated with the given blog ID.
14245 *
14246 * @author Leo Fajardo (@leorw)
14247 */
14248 $fs->switch_to_blog( $blog_id );
14249 }
14250
14251 if ( is_object( $user ) ) {
14252 if ( $is_network_activation_or_migration && ! $has_valid_blog_id ) {
14253 // If no specific blog ID was provided, activate the license for all sites in the network.
14254 $blog_2_install_map = array();
14255 $site_ids = array();
14256
14257 foreach ( $sites as $site ) {
14258 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
14259 continue;
14260 }
14261
14262 $install = $fs->get_install_by_blog_id( $site['blog_id'] );
14263
14264 if ( is_object( $install ) ) {
14265 $blog_2_install_map[ $site['blog_id'] ] = $install;
14266 } else {
14267 $site_ids[] = $site['blog_id'];
14268 }
14269 }
14270
14271 if ( ! empty( $blog_2_install_map ) ) {
14272 $result = $fs->activate_license_on_many_installs( $user, $license_key, $blog_2_install_map );
14273
14274 if ( true !== $result ) {
14275 $error = FS_Api::is_api_error_object( $result ) ?
14276 $result->error->message :
14277 var_export( $result, true );
14278 }
14279 }
14280
14281 if ( empty( $error ) && ! empty( $site_ids ) ) {
14282 $result = $fs->activate_license_on_many_sites( $user, $license_key, $site_ids );
14283
14284 if ( true !== $result ) {
14285 $error = FS_Api::is_api_error_object( $result ) ?
14286 $result->error->message :
14287 var_export( $result, true );
14288 }
14289 }
14290 } else {
14291 if ( $fs->is_registered() ) {
14292 $params = array(
14293 'license_key' => $fs->apply_filters( 'license_key', $license_key )
14294 );
14295
14296 $install_ids = array();
14297
14298 $change_owner = FS_User::is_valid_id( $license_owner_id );
14299
14300 if ( $change_owner ) {
14301 $params['user_id'] = $license_owner_id;
14302
14303 $installs_info_by_slug_map = $fs->get_parent_and_addons_installs_info();
14304
14305 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
14306 $install_ids[ $slug ] = $install_info['install']->id;
14307 }
14308
14309 $params['install_ids'] = implode( ',', array_values( $install_ids ) );
14310 }
14311
14312 $api = $fs->get_api_site_scope();
14313
14314 $install = $api->call( $fs->add_show_pending( '/' ), 'put', $params );
14315
14316 if ( FS_Api::is_api_error( $install ) ) {
14317 $error = FS_Api::is_api_error_object( $install ) ?
14318 $install->error->message :
14319 var_export( $install->error, true );
14320 } else {
14321 $fs->reconnect_locally( $has_valid_blog_id );
14322
14323 if (
14324 $change_owner &&
14325 // If successful ownership change.
14326 $fs->get_user()->id != $install->user_id
14327 ) {
14328 $fs->complete_ownership_change_by_license( $install->user_id, $install_ids );
14329 }
14330 }
14331 } else /* ( $fs->is_addon() && $fs->get_parent_instance()->is_registered() ) */ {
14332 $result = $fs->activate_license_on_site( $user, $license_key );
14333
14334 if ( true !== $result ) {
14335 $error = FS_Api::is_api_error_object( $result ) ?
14336 $result->error->message :
14337 var_export( $result, true );
14338 }
14339 }
14340 }
14341
14342 if ( empty( $error ) ) {
14343 $fs->network_upgrade_mode_completed();
14344
14345 $fs->_user = $user;
14346
14347 if ( fs_is_network_admin() && ! $has_valid_blog_id ) {
14348 $fs->_site = $fs->get_network_install();
14349 }
14350
14351 $fs->_sync_license( true, $has_valid_blog_id );
14352
14353 $this->maybe_sync_install_user();
14354
14355 $next_page = $fs->is_addon() ?
14356 $fs->get_parent_instance()->get_account_url() :
14357 $fs->get_after_activation_url( 'after_connect_url' );
14358 }
14359 } else {
14360 $next_page = $fs->opt_in(
14361 false,
14362 false,
14363 false,
14364 $license_key,
14365 false,
14366 false,
14367 false,
14368 $is_marketing_allowed,
14369 $sites
14370 );
14371
14372 if ( isset( $next_page->error ) ) {
14373 $error = $next_page->error;
14374 } else {
14375 if ( $is_network_activation_or_migration ) {
14376 /**
14377 * Get the list of sites that were just opted-in (and license activated).
14378 * This is an optimization for the next part below saving some DB queries.
14379 */
14380 $connected_sites = array();
14381 foreach ( $sites as $site ) {
14382 if ( isset( $site['blog_id'] ) && is_numeric( $site['blog_id'] ) ) {
14383 $connected_sites[ $site['blog_id'] ] = true;
14384 }
14385 }
14386
14387 $all_sites = self::get_sites();
14388 $pending_blog_ids = array();
14389
14390 /**
14391 * 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.
14392 *
14393 * @author Vova Feldman (@svovaf)
14394 */
14395 foreach ( $all_sites as $site ) {
14396 $blog_id = self::get_site_blog_id( $site );
14397
14398 if ( isset( $connected_sites[ $blog_id ] ) ) {
14399 // Site was just connected.
14400 continue;
14401 }
14402
14403 if ( $fs->is_installed_on_site( $blog_id ) ) {
14404 // Site was already connected before.
14405 continue;
14406 }
14407
14408 if ( $fs->is_site_delegated_connection( $blog_id ) ) {
14409 // Site's connection was delegated.
14410 continue;
14411 }
14412
14413 if ( $fs->is_anonymous_site( $blog_id ) ) {
14414 // Site connection was already skipped.
14415 continue;
14416 }
14417
14418 $pending_blog_ids[] = $blog_id;
14419 }
14420
14421 if ( ! empty( $pending_blog_ids ) ) {
14422 if ( $fs->is_freemium() && $fs->is_enable_anonymous() ) {
14423 $fs->skip_connection( $pending_blog_ids );
14424 } else {
14425 $fs->delegate_connection( $pending_blog_ids );
14426 }
14427 }
14428 }
14429 }
14430 }
14431
14432 if ( false === $error && true === $fs->_storage->require_license_activation ) {
14433 $fs->_storage->require_license_activation = false;
14434 }
14435
14436 $result = array(
14437 'success' => ( false === $error )
14438 );
14439
14440 if ( false !== $error ) {
14441 $result['error'] = $fs->apply_filters( 'opt_in_error_message', $error );
14442 } else {
14443 if ( $fs->is_addon() || $fs->has_addons() ) {
14444 /**
14445 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
14446 * an updated valid user licenses collection will be fetched from the server which is used to also
14447 * update the account add-ons (add-ons the user has licenses for).
14448 *
14449 * @author Leo Fajardo (@leorw)
14450 * @since 2.2.4
14451 */
14452 $fs->purge_valid_user_licenses_cache();
14453 }
14454
14455 $result['next_page'] = $next_page;
14456 }
14457
14458 return $result;
14459 }
14460
14461 /**
14462 * @author Leo Fajardo (@leorw)
14463 * @since 2.3.2
14464 *
14465 * @return array {
14466 * @key string Product slug.
14467 * @value array {
14468 * @property FS_Site $site
14469 * @property FS_Plugin_License $license
14470 * }
14471 * }
14472 */
14473 private function get_parent_and_addons_installs_info() {
14474 $fs = $this->is_addon() ?
14475 $this->get_parent_instance() :
14476 $this;
14477
14478 $installed_addons_ids = array();
14479
14480 $installed_addons_instances = $fs->get_installed_addons();
14481 foreach ( $installed_addons_instances as $instance ) {
14482 $installed_addons_ids[] = $instance->get_id();
14483 }
14484
14485 $addons_ids = array_unique( array_merge(
14486 $installed_addons_ids,
14487 $fs->get_updated_account_addons()
14488 ) );
14489
14490 // Add parent product info.
14491 $installs_info_by_slug_map = array(
14492 $fs->get_slug() => array(
14493 'install' => $fs->get_site(),
14494 'license' => $fs->_get_license()
14495 )
14496 );
14497
14498 foreach ( $addons_ids as $addon_id ) {
14499 $is_installed = isset( $installed_addons_ids_map[ $addon_id ] );
14500
14501 $addon_info = $fs->_get_addon_info( $addon_id, $is_installed );
14502
14503 if ( ! isset( $addon_info['is_connected'] ) || ! $addon_info['is_connected'] ) {
14504 // Add-on is not associated with an install entity.
14505 continue;
14506 }
14507
14508 $installs_info_by_slug_map[ $addon_info['slug'] ] = array(
14509 'install' => $addon_info['site'],
14510 'license' => isset( $addon_info['license'] ) ?
14511 $addon_info['license'] :
14512 null
14513 );
14514 }
14515
14516 return $installs_info_by_slug_map;
14517 }
14518
14519 /**
14520 * @author Leo Fajardo (@leorw)
14521 * @since 1.2.3.1
14522 */
14523 function _network_activate_ajax_action() {
14524 $this->_logger->entrance();
14525
14526 $this->check_ajax_referer( 'network_activate' );
14527
14528 $plugin_id = fs_request_get( 'module_id', '', 'post' );
14529 $fs = ( $plugin_id == $this->_module_id ) ?
14530 $this :
14531 $this->get_addon_instance( $plugin_id );
14532
14533 $error = false;
14534
14535 $sites = fs_request_get( 'sites', array(), 'post' );
14536 if ( is_array( $sites ) && ! empty( $sites ) ) {
14537 $sites_by_action = array(
14538 'allow' => array(),
14539 'delegate' => array(),
14540 'skip' => array()
14541 );
14542
14543 foreach ( $sites as $site ) {
14544 $sites_by_action[ $site['action'] ][] = $site;
14545 }
14546
14547 $total_sites = count( $sites );
14548 $total_sites_to_delegate = count( $sites_by_action['delegate'] );
14549
14550 $next_page = '';
14551
14552 $has_any_install = fs_request_get_bool( 'has_any_install' );
14553
14554 if ( $total_sites === $total_sites_to_delegate &&
14555 ! $this->is_network_upgrade_mode() &&
14556 ! $has_any_install
14557 ) {
14558 $this->delegate_connection();
14559 } else {
14560 if ( ! empty( $sites_by_action['delegate'] ) ) {
14561 $this->delegate_connection( self::get_sites_blog_ids( $sites_by_action['delegate'] ) );
14562 }
14563
14564 if ( ! empty( $sites_by_action['skip'] ) ) {
14565 $this->skip_connection( self::get_sites_blog_ids( $sites_by_action['skip'] ) );
14566 }
14567
14568 if ( empty( $sites_by_action['allow'] ) ) {
14569 if ( $has_any_install ) {
14570 $first_install = $fs->find_first_install();
14571
14572 if ( ! is_null( $first_install ) ) {
14573 $fs->_site = $first_install['install'];
14574 $fs->_storage->network_install_blog_id = $first_install['blog_id'];
14575
14576 $fs->_user = self::_get_user_by_id( $fs->_site->user_id );
14577 $fs->_storage->network_user_id = $fs->_user->id;
14578 }
14579 }
14580 } else {
14581 if ( ! $fs->is_registered() || ! $this->_is_network_active ) {
14582 $next_page = $fs->opt_in(
14583 false,
14584 false,
14585 false,
14586 false,
14587 false,
14588 false,
14589 false,
14590 fs_request_get_bool( 'is_marketing_allowed', null ),
14591 $sites_by_action['allow']
14592 );
14593 } else {
14594 $next_page = $fs->install_with_user(
14595 $this->get_network_user(),
14596 false,
14597 false,
14598 false,
14599 true,
14600 $sites_by_action['allow']
14601 );
14602 }
14603
14604 if ( is_object( $next_page ) && isset( $next_page->error ) ) {
14605 $error = $next_page->error;
14606 }
14607 }
14608 }
14609
14610 if ( empty( $next_page ) ) {
14611 $next_page = $this->get_after_activation_url( 'after_network_activation_url' );
14612 }
14613 } else {
14614 $error = $this->get_text_inline( 'Invalid site details collection.', 'invalid_site_details_collection' );
14615 }
14616
14617 $result = array(
14618 'success' => ( false === $error )
14619 );
14620
14621 if ( false !== $error ) {
14622 $result['error'] = $error;
14623 } else {
14624 $result['next_page'] = $next_page;
14625 }
14626
14627 echo json_encode( $result );
14628
14629 exit;
14630 }
14631
14632 /**
14633 * Billing update AJAX callback.
14634 *
14635 * @author Vova Feldman (@svovaf)
14636 * @since 1.2.1.5
14637 */
14638 function _update_billing_ajax_action() {
14639 $this->_logger->entrance();
14640
14641 $this->check_ajax_referer( 'update_billing' );
14642
14643 if ( ! $this->is_user_admin() ) {
14644 // Only for admins.
14645 self::shoot_ajax_failure();
14646 }
14647
14648 $billing = fs_request_get( 'billing' );
14649
14650 $api = $this->get_api_user_scope();
14651 $result = $api->call( '/billing.json', 'put', array_merge( $billing, array(
14652 'plugin_id' => $this->get_parent_id(),
14653 ) ) );
14654
14655 if ( ! $this->is_api_result_entity( $result ) ) {
14656 self::shoot_ajax_failure();
14657 }
14658
14659 // Purge cached billing.
14660 $this->get_api_user_scope()->purge_cache( 'billing.json' );
14661
14662 self::shoot_ajax_success();
14663 }
14664
14665 /**
14666 * Trial start for anonymous users (AJAX callback).
14667 *
14668 * @author Vova Feldman (@svovaf)
14669 * @since 1.2.1.5
14670 */
14671 function _start_trial_ajax_action() {
14672 $this->_logger->entrance();
14673
14674 $this->check_ajax_referer( 'start_trial' );
14675
14676 if ( ! $this->is_user_admin() ) {
14677 // Only for admins.
14678 self::shoot_ajax_failure();
14679 }
14680
14681 $trial_data = fs_request_get( 'trial' );
14682
14683 $next_page = $this->opt_in(
14684 false,
14685 false,
14686 false,
14687 false,
14688 false,
14689 $trial_data['plan_id']
14690 );
14691
14692 if ( is_object( $next_page ) && $this->is_api_error( $next_page ) ) {
14693 self::shoot_ajax_failure(
14694 isset( $next_page->error ) ?
14695 $next_page->error->message :
14696 var_export( $next_page, true )
14697 );
14698 }
14699
14700 $this->shoot_ajax_success( array(
14701 'next_page' => $next_page,
14702 ) );
14703 }
14704
14705 /**
14706 * @author Leo Fajardo (@leorw)
14707 * @since 1.2.0
14708 */
14709 function _resend_license_key_ajax_action() {
14710 $this->_logger->entrance();
14711
14712 $this->check_ajax_referer( 'resend_license_key' );
14713
14714 $email_address = sanitize_email( trim( fs_request_get( 'email', '', 'post' ) ) );
14715
14716 if ( empty( $email_address ) ) {
14717 exit;
14718 }
14719
14720 $error = false;
14721
14722 $api = $this->get_api_plugin_scope();
14723 $result = $api->call( '/licenses/resend.json', 'post',
14724 array(
14725 'email' => $email_address,
14726 'url' => home_url(),
14727 )
14728 );
14729
14730 if ( is_object( $result ) && isset( $result->error ) ) {
14731 $error = $result->error;
14732
14733 if ( in_array( $error->code, array( 'invalid_email', 'no_user' ) ) ) {
14734 $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' );
14735 } else if ( 'no_license' === $error->code ) {
14736 $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' );
14737 } else {
14738 $error = $error->message;
14739 }
14740 }
14741
14742 $licenses = array(
14743 'success' => ( false === $error )
14744 );
14745
14746 if ( false !== $error ) {
14747 $licenses['error'] = sprintf( '%s... %s', $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ), strtolower( $error ) );
14748 }
14749
14750 echo json_encode( $licenses );
14751
14752 exit;
14753 }
14754
14755 /**
14756 * @author Vova Feldman (@svovaf)
14757 * @since 1.2.1.8
14758 *
14759 * @var string
14760 */
14761 private static $_pagenow;
14762
14763 /**
14764 * Get current page or the referer if executing a WP AJAX request.
14765 *
14766 * @author Vova Feldman (@svovaf)
14767 * @since 1.2.1.8
14768 *
14769 * @return string
14770 */
14771 static function get_current_page() {
14772 if ( ! isset( self::$_pagenow ) ) {
14773 global $pagenow;
14774 if ( empty( $pagenow ) && is_admin() && is_multisite() ) {
14775 /**
14776 * It appears that `$pagenow` is not yet initialized in some network admin pages when this method
14777 * is called, so initialize it here using some pieces of code from `wp-includes/vars.php`.
14778 *
14779 * @author Leo Fajardo (@leorw)
14780 * @since 2.2.3
14781 */
14782 if ( is_network_admin() ) {
14783 preg_match( '#/wp-admin/network/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14784 } else if ( is_user_admin() ) {
14785 preg_match( '#/wp-admin/user/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14786 } else {
14787 preg_match( '#/wp-admin/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
14788 }
14789
14790 $pagenow = $self_matches[1];
14791 $pagenow = trim( $pagenow, '/' );
14792 $pagenow = preg_replace( '#\?.*?$#', '', $pagenow );
14793 if ( '' === $pagenow || 'index' === $pagenow || 'index.php' === $pagenow ) {
14794 $pagenow = 'index.php';
14795 } else {
14796 preg_match( '#(.*?)(/|$)#', $pagenow, $self_matches );
14797 $pagenow = strtolower( $self_matches[1] );
14798 if ( '.php' !== substr($pagenow, -4, 4) )
14799 $pagenow .= '.php'; // for Options +Multiviews: /wp-admin/themes/index.php (themes.php is queried)
14800 }
14801 }
14802
14803 self::$_pagenow = $pagenow;
14804
14805 if ( self::is_ajax() &&
14806 'admin-ajax.php' === $pagenow
14807 ) {
14808 $referer = fs_get_raw_referer();
14809
14810 if ( is_string( $referer ) ) {
14811 $parts = explode( '?', $referer );
14812
14813 self::$_pagenow = basename( $parts[0] );
14814 }
14815 }
14816 }
14817
14818 return self::$_pagenow;
14819 }
14820
14821 /**
14822 * Helper method to check if user in the plugins page.
14823 *
14824 * @author Vova Feldman (@svovaf)
14825 * @since 1.2.1.5
14826 *
14827 * @return bool
14828 */
14829 static function is_plugins_page() {
14830 return ( 'plugins.php' === self::get_current_page() );
14831 }
14832
14833 /**
14834 * @author Leo Fajardo (@leorw)
14835 * @since 2.2.3
14836 *
14837 * @return bool
14838 */
14839 static function is_plugin_install_page() {
14840 return ( 'plugin-install.php' === self::get_current_page() );
14841 }
14842
14843 /**
14844 * @author Leo Fajardo (@leorw)
14845 * @since 2.0.2
14846 *
14847 * @return bool
14848 */
14849 static function is_updates_page() {
14850 return ( 'update-core.php' === self::get_current_page() );
14851 }
14852
14853 /**
14854 * Helper method to check if user in the themes page.
14855 *
14856 * @author Vova Feldman (@svovaf)
14857 * @since 1.2.2.6
14858 *
14859 * @return bool
14860 */
14861 static function is_themes_page() {
14862 return ( 'themes.php' === self::get_current_page() );
14863 }
14864
14865 #----------------------------------------------------------------------------------
14866 #region Affiliation
14867 #----------------------------------------------------------------------------------
14868
14869 /**
14870 * @author Leo Fajardo (@leorw)
14871 * @since 1.2.3
14872 *
14873 * @return bool
14874 */
14875 function has_affiliate_program() {
14876 if ( ! is_object( $this->_plugin ) ) {
14877 return false;
14878 }
14879
14880 return $this->_plugin->has_affiliate_program();
14881 }
14882
14883 /**
14884 * Get Plugin ID under which we will track affiliate application.
14885 *
14886 * This could either be the Bundle ID or the main plugin ID.
14887 *
14888 * @return number Bundle ID if developer has provided one, else the main plugin ID.
14889 */
14890 private function get_plugin_id_for_affiliate_terms() {
14891 return $this->has_bundle_context() ?
14892 $this->get_bundle_id() :
14893 $this->_plugin_id;
14894 }
14895
14896 /**
14897 * @author Leo Fajardo (@leorw)
14898 * @since 1.2.4
14899 */
14900 private function fetch_affiliate_terms() {
14901 if ( ! is_object( $this->plugin_affiliate_terms ) ) {
14902 /**
14903 * In case we have a bundle set in SDK configuration, we would like to use that for affiliates, not the main plugin.
14904 */
14905 $plugins_api = $this->has_bundle_context() ?
14906 $this->get_api_bundle_scope() :
14907 $this->get_api_plugin_scope();
14908
14909 $affiliate_terms = $plugins_api->get( '/aff.json?type=affiliation', false );
14910
14911 /**
14912 * 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.
14913 */
14914 if ( ! $this->is_api_result_entity( $affiliate_terms ) ) {
14915 return;
14916 }
14917
14918 $this->plugin_affiliate_terms = new FS_AffiliateTerms( $affiliate_terms );
14919 }
14920 }
14921
14922 /**
14923 * @author Leo Fajardo (@leorw)
14924 * @since 1.2.4
14925 */
14926 private function fetch_affiliate_and_custom_terms() {
14927 if ( ! empty( $this->_storage->affiliate_application_data ) ) {
14928 $application_data = $this->_storage->affiliate_application_data;
14929 $flush = ( ! isset( $application_data['status'] ) || 'pending' === $application_data['status'] );
14930
14931 $plugin_id_for_affiliate = $this->get_plugin_id_for_affiliate_terms();
14932
14933 $users_api = $this->get_api_user_scope();
14934 $result = $users_api->get( "/plugins/{$plugin_id_for_affiliate}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json", $flush );
14935 if ( $this->is_api_result_object( $result, 'affiliates' ) ) {
14936 if ( ! empty( $result->affiliates ) ) {
14937 $affiliate = new FS_Affiliate( $result->affiliates[0] );
14938
14939 if ( ! isset( $application_data['status'] ) || $application_data['status'] !== $affiliate->status ) {
14940 $application_data['status'] = $affiliate->status;
14941 $this->_storage->affiliate_application_data = $application_data;
14942 }
14943
14944 if ( $affiliate->is_using_custom_terms ) {
14945 $affiliate_terms = $users_api->get( "/plugins/{$this->_plugin->id}/affiliates/{$affiliate->id}/aff/{$affiliate->custom_affiliate_terms_id}.json", $flush );
14946 if ( $this->is_api_result_entity( $affiliate_terms ) ) {
14947 $this->custom_affiliate_terms = new FS_AffiliateTerms( $affiliate_terms );
14948 }
14949 }
14950
14951 $this->affiliate = $affiliate;
14952 }
14953 }
14954 }
14955 }
14956
14957 /**
14958 * @author Leo Fajardo (@leorw)
14959 * @since 1.2.3
14960 */
14961 private function fetch_affiliate_and_terms() {
14962 $this->_logger->entrance();
14963
14964 $this->fetch_affiliate_terms();
14965 $this->fetch_affiliate_and_custom_terms();
14966 }
14967
14968 /**
14969 * @author Leo Fajardo (@leorw)
14970 * @since 1.2.3
14971 *
14972 * @return FS_Affiliate
14973 */
14974 function get_affiliate() {
14975 return $this->affiliate;
14976 }
14977
14978
14979 /**
14980 * @author Leo Fajardo (@leorw)
14981 * @since 1.2.3
14982 *
14983 * @return FS_AffiliateTerms
14984 */
14985 function get_affiliate_terms() {
14986 return is_object( $this->custom_affiliate_terms ) ?
14987 $this->custom_affiliate_terms :
14988 $this->plugin_affiliate_terms;
14989 }
14990
14991 /**
14992 * @author Leo Fajardo (@leorw)
14993 * @since 1.2.3
14994 */
14995 function _submit_affiliate_application() {
14996 $this->_logger->entrance();
14997
14998 $this->check_ajax_referer( 'submit_affiliate_application' );
14999
15000 if ( ! $this->is_user_admin() ) {
15001 // Only for admins.
15002 self::shoot_ajax_failure();
15003 }
15004
15005 $affiliate = fs_request_get( 'affiliate' );
15006
15007 if ( empty( $affiliate['promotion_methods'] ) ) {
15008 unset( $affiliate['promotion_methods'] );
15009 }
15010
15011 if ( ! empty( $affiliate['additional_domains'] ) ) {
15012 $affiliate['additional_domains'] = array_unique( $affiliate['additional_domains'] );
15013 }
15014
15015 if ( ! $this->is_registered() ) {
15016 // Opt in but don't track usage.
15017 $next_page = $this->opt_in(
15018 false,
15019 false,
15020 false,
15021 false,
15022 false,
15023 false,
15024 true
15025 );
15026
15027 if ( is_object( $next_page ) && $this->is_api_error( $next_page ) ) {
15028 self::shoot_ajax_failure(
15029 isset( $next_page->error ) ?
15030 $next_page->error->message :
15031 var_export( $next_page, true )
15032 );
15033 } else if ( $this->is_pending_activation() ) {
15034 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' ) );
15035 }
15036 }
15037
15038 $this->fetch_affiliate_terms();
15039
15040 $plugin_id_for_affiliate = $this->get_plugin_id_for_affiliate_terms();
15041
15042 $api = $this->get_api_user_scope();
15043 $result = $api->call(
15044 ( "/plugins/{$plugin_id_for_affiliate}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json" ),
15045 'post',
15046 $affiliate
15047 );
15048
15049 if ( $this->is_api_error( $result ) ) {
15050 self::shoot_ajax_failure(
15051 isset( $result->error ) ?
15052 $result->error->message :
15053 var_export( $result, true )
15054 );
15055 } else {
15056 if ( $this->_admin_notices->has_sticky( 'affiliate_program' ) ) {
15057 $this->_admin_notices->remove_sticky( 'affiliate_program' );
15058 }
15059
15060 $affiliate_application_data = array(
15061 'status' => 'pending',
15062 'stats_description' => $affiliate['stats_description'],
15063 'promotion_method_description' => $affiliate['promotion_method_description'],
15064 );
15065
15066 if ( ! empty( $affiliate['promotion_methods'] ) ) {
15067 $affiliate_application_data['promotion_methods'] = $affiliate['promotion_methods'];
15068 }
15069
15070 if ( ! empty( $affiliate['domain'] ) ) {
15071 $affiliate_application_data['domain'] = $affiliate['domain'];
15072 }
15073
15074 if ( ! empty( $affiliate['additional_domains'] ) ) {
15075 $affiliate_application_data['additional_domains'] = $affiliate['additional_domains'];
15076 }
15077
15078 $this->_storage->affiliate_application_data = $affiliate_application_data;
15079 }
15080
15081 // Purge cached affiliate.
15082 $api->purge_cache( 'affiliate.json' );
15083
15084 self::shoot_ajax_success( $result );
15085 }
15086
15087 /**
15088 * @author Leo Fajardo (@leorw)
15089 * @since 1.2.3
15090 *
15091 * @return array|null
15092 */
15093 function get_affiliate_application_data() {
15094 if ( empty( $this->_storage->affiliate_application_data ) ) {
15095 return null;
15096 }
15097
15098 return $this->_storage->affiliate_application_data;
15099 }
15100
15101 #endregion Affiliation ------------------------------------------------------------
15102
15103 #----------------------------------------------------------------------------------
15104 #region URL Generators
15105 #----------------------------------------------------------------------------------
15106
15107 /**
15108 * Alias to pricing_url().
15109 *
15110 * @author Vova Feldman (@svovaf)
15111 * @since 1.0.2
15112 *
15113 * @uses pricing_url()
15114 *
15115 * @param string $period Billing cycle
15116 * @param bool $is_trial
15117 *
15118 * @return string
15119 */
15120 function get_upgrade_url( $period = WP_FS__PERIOD_ANNUALLY, $is_trial = false ) {
15121 return $this->pricing_url( $period, $is_trial );
15122 }
15123
15124 /**
15125 * @author Vova Feldman (@svovaf)
15126 * @since 1.0.9
15127 *
15128 * @uses get_upgrade_url()
15129 *
15130 * @return string
15131 */
15132 function get_trial_url() {
15133 return $this->get_upgrade_url( WP_FS__PERIOD_ANNUALLY, true );
15134 }
15135
15136 /**
15137 * @author Leo Fajardo (@leorw)
15138 * @since 2.1.4
15139 *
15140 * @param string $new_version
15141 *
15142 * @return string
15143 */
15144 function version_upgrade_checkout_link( $new_version ) {
15145 if ( ! is_object( $this->_license ) ) {
15146 $url = $this->pricing_url();
15147
15148 $purchase_license_text = $this->get_text_inline( 'Buy a license now', 'buy-license-now' );
15149 } else {
15150 $subscription = $this->_get_subscription( $this->_license->id );
15151
15152 $url = $this->checkout_url(
15153 is_object( $subscription ) ?
15154 ( 1 == $subscription->billing_cycle ? WP_FS__PERIOD_MONTHLY : WP_FS__PERIOD_ANNUALLY ) :
15155 WP_FS__PERIOD_LIFETIME,
15156 false,
15157 array( 'licenses' => $this->_license->quota )
15158 );
15159
15160 $purchase_license_text = $this->get_text_inline( 'Renew your license now', 'renew-license-now' );
15161 }
15162
15163 return sprintf(
15164 $this->get_text_inline( '%s to access version %s security & feature updates, and support.', 'x-for-updates-and-support' ),
15165 sprintf( '<a href="%s">%s</a>', $url, $purchase_license_text ),
15166 $new_version
15167 );
15168 }
15169
15170 /**
15171 * Plugin's pricing URL.
15172 *
15173 * @author Vova Feldman (@svovaf)
15174 * @since 1.0.4
15175 *
15176 * @param string $billing_cycle Billing cycle
15177 *
15178 * @param bool $is_trial
15179 *
15180 * @return string
15181 */
15182 function pricing_url( $billing_cycle = WP_FS__PERIOD_ANNUALLY, $is_trial = false ) {
15183 $this->_logger->entrance();
15184
15185 $params = array(
15186 'billing_cycle' => $billing_cycle
15187 );
15188
15189 if ( $is_trial ) {
15190 $params['trial'] = 'true';
15191 }
15192
15193 $url = $this->is_addon() ?
15194 $this->_parent->addon_url( $this->_slug ) :
15195 $this->_get_admin_page_url( 'pricing', $params );
15196
15197 return $this->apply_filters( 'pricing_url', $url );
15198 }
15199
15200 /**
15201 * Checkout page URL.
15202 *
15203 * @author Vova Feldman (@svovaf)
15204 * @since 1.0.6
15205 *
15206 * @param string $billing_cycle Billing cycle
15207 * @param bool $is_trial
15208 * @param array $extra (optional) Extra parameters, override other query params.
15209 * @param bool|null $network
15210 *
15211 * @return string
15212 */
15213 function checkout_url(
15214 $billing_cycle = WP_FS__PERIOD_ANNUALLY,
15215 $is_trial = false,
15216 $extra = array(),
15217 $network = null
15218 ) {
15219 $this->_logger->entrance();
15220
15221 $params = array(
15222 'checkout' => 'true',
15223 'billing_cycle' => $billing_cycle,
15224 );
15225
15226 if ( $is_trial ) {
15227 $params['trial'] = 'true';
15228 }
15229
15230 /**
15231 * Params in extra override other params.
15232 */
15233 $params = array_merge( $params, $extra );
15234
15235 return $this->_get_admin_page_url( 'pricing', $params, $network );
15236 }
15237
15238 /**
15239 * Add-on checkout URL.
15240 *
15241 * @author Vova Feldman (@svovaf)
15242 * @since 1.1.7
15243 *
15244 * @param number $addon_id
15245 * @param number $pricing_id
15246 * @param string $billing_cycle
15247 * @param bool $is_trial
15248 * @param bool|null $network
15249 *
15250 * @return string
15251 */
15252 function addon_checkout_url(
15253 $addon_id,
15254 $pricing_id,
15255 $billing_cycle = WP_FS__PERIOD_ANNUALLY,
15256 $is_trial = false,
15257 $network = null
15258 ) {
15259 return $this->checkout_url( $billing_cycle, $is_trial, array(
15260 'plugin_id' => $addon_id,
15261 'pricing_id' => $pricing_id,
15262 ), $network );
15263 }
15264
15265 #endregion
15266
15267 #endregion ------------------------------------------------------------------
15268
15269 /**
15270 * Check if plugin has any add-ons.
15271 *
15272 * @author Vova Feldman (@svovaf)
15273 * @since 1.0.5
15274 *
15275 * @since 1.1.7.3 Base logic only on the parameter provided by the developer in the init function.
15276 *
15277 * @return bool
15278 */
15279 function has_addons() {
15280 $this->_logger->entrance();
15281
15282 return $this->_has_addons;
15283 }
15284
15285 /**
15286 * Check if plugin can work in anonymous mode.
15287 *
15288 * @author Vova Feldman (@svovaf)
15289 * @since 1.0.9
15290 *
15291 * @return bool
15292 *
15293 * @deprecated Please use is_enable_anonymous() instead.
15294 */
15295 function enable_anonymous() {
15296 return $this->_enable_anonymous;
15297 }
15298
15299 /**
15300 * Check if plugin can work in anonymous mode.
15301 *
15302 * @author Vova Feldman (@svovaf)
15303 * @since 1.1.9
15304 *
15305 * @return bool
15306 */
15307 function is_enable_anonymous() {
15308 return $this->_enable_anonymous;
15309 }
15310
15311 /**
15312 * Check if plugin is premium only (no free plans).
15313 *
15314 * @author Vova Feldman (@svovaf)
15315 * @since 1.1.9
15316 *
15317 * @return bool
15318 */
15319 function is_only_premium() {
15320 return $this->_is_premium_only;
15321 }
15322
15323 /**
15324 * Checks if the plugin's type is "plugin". The other type is "theme".
15325 *
15326 * @author Leo Fajardo (@leorw)
15327 * @since 1.2.2
15328 *
15329 * @return bool
15330 */
15331 function is_plugin() {
15332 return ( WP_FS__MODULE_TYPE_PLUGIN === $this->_module_type );
15333 }
15334
15335 /**
15336 * @author Leo Fajardo (@leorw)
15337 * @since 1.2.2
15338 *
15339 * @return string
15340 */
15341 function get_module_type() {
15342 if ( ! isset( $this->_module_type ) ) {
15343 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
15344 $this->_module_type = $id_slug_type_path_map[ $this->_module_id ]['type'];
15345 }
15346
15347 return $this->_module_type;
15348 }
15349
15350 /**
15351 * @author Leo Fajardo (@leorw)
15352 * @since 1.2.2
15353 *
15354 * @return string
15355 */
15356 function get_plugin_main_file_path() {
15357 return $this->_plugin_main_file_path;
15358 }
15359
15360 /**
15361 * Check if module has a premium code version.
15362 *
15363 * Serviceware module might be freemium without any
15364 * premium code version, where the paid features
15365 * are all part of the service.
15366 *
15367 * @author Vova Feldman (@svovaf)
15368 * @since 1.2.1.6
15369 *
15370 * @return bool
15371 */
15372 function has_premium_version() {
15373 return $this->_has_premium_version;
15374 }
15375
15376 /**
15377 * Check if feature supported with current site's plan.
15378 *
15379 * @author Vova Feldman (@svovaf)
15380 * @since 1.0.1
15381 *
15382 * @todo IMPLEMENT
15383 *
15384 * @param number $feature_id
15385 *
15386 * @throws Exception
15387 */
15388 function is_feature_supported( $feature_id ) {
15389 throw new Exception( 'not implemented' );
15390 }
15391
15392 /**
15393 * @author Vova Feldman (@svovaf)
15394 * @since 1.0.1
15395 *
15396 * @return bool Is running in SSL/HTTPS
15397 */
15398 function is_ssl() {
15399 return WP_FS__IS_HTTPS;
15400 }
15401
15402 /**
15403 * @author Vova Feldman (@svovaf)
15404 * @since 1.0.9
15405 *
15406 * @return bool Is running in AJAX call.
15407 *
15408 * @link http://wordpress.stackexchange.com/questions/70676/how-to-check-if-i-am-in-admin-ajax
15409 */
15410 static function is_ajax() {
15411 return ( defined( 'DOING_AJAX' ) && DOING_AJAX );
15412 }
15413
15414 /**
15415 * Check if it's an AJAX call targeted for the current module.
15416 *
15417 * @author Vova Feldman (@svovaf)
15418 * @since 1.2.0
15419 *
15420 * @param array|string $actions Collection of AJAX actions.
15421 *
15422 * @return bool
15423 */
15424 function is_ajax_action( $actions ) {
15425 // Verify it's an ajax call.
15426 if ( ! self::is_ajax() ) {
15427 return false;
15428 }
15429
15430 // Verify the call is relevant for the plugin.
15431 if ( $this->_module_id != fs_request_get( 'module_id' ) ) {
15432 return false;
15433 }
15434
15435 // Verify it's one of the specified actions.
15436 if ( is_string( $actions ) ) {
15437 $actions = explode( ',', $actions );
15438 }
15439
15440 if ( is_array( $actions ) && 0 < count( $actions ) ) {
15441 $ajax_action = fs_request_get( 'action' );
15442
15443 foreach ( $actions as $action ) {
15444 if ( $ajax_action === $this->get_action_tag( $action ) ) {
15445 return true;
15446 }
15447 }
15448 }
15449
15450 return false;
15451 }
15452
15453 /**
15454 * Check if it's an AJAX call targeted for current request.
15455 *
15456 * @author Vova Feldman (@svovaf)
15457 * @since 1.2.0
15458 *
15459 * @param array|string $actions Collection of AJAX actions.
15460 * @param number|null $module_id
15461 *
15462 * @return bool
15463 */
15464 static function is_ajax_action_static( $actions, $module_id = null ) {
15465 // Verify it's an ajax call.
15466 if ( ! self::is_ajax() ) {
15467 return false;
15468 }
15469
15470
15471 if ( ! empty( $module_id ) ) {
15472 // Verify the call is relevant for the plugin.
15473 if ( $module_id != fs_request_get( 'module_id' ) ) {
15474 return false;
15475 }
15476 }
15477
15478 // Verify it's one of the specified actions.
15479 if ( is_string( $actions ) ) {
15480 $actions = explode( ',', $actions );
15481 }
15482
15483 if ( is_array( $actions ) && 0 < count( $actions ) ) {
15484 $ajax_action = fs_request_get( 'action' );
15485
15486 foreach ( $actions as $action ) {
15487 if ( $ajax_action === self::get_ajax_action_static( $action, $module_id ) ) {
15488 return true;
15489 }
15490 }
15491 }
15492
15493 return false;
15494 }
15495
15496 /**
15497 * @author Vova Feldman (@svovaf)
15498 * @since 1.1.7
15499 *
15500 * @return bool
15501 */
15502 static function is_cron() {
15503 return ( defined( 'DOING_CRON' ) && DOING_CRON );
15504 }
15505
15506 /**
15507 * @author Leo Fajardo (@leorw)
15508 * @since 2.5.0
15509 *
15510 * @return bool
15511 */
15512 static function is_admin_post() {
15513 return ( 'admin-post.php' === self::get_current_page() );
15514 }
15515
15516 /**
15517 * Check if a real user is visiting the admin dashboard.
15518 *
15519 * @author Vova Feldman (@svovaf)
15520 * @since 1.1.7
15521 *
15522 * @return bool
15523 */
15524 function is_user_in_admin() {
15525 return (
15526 is_admin() &&
15527 ! self::is_ajax() &&
15528 ! self::is_cron() &&
15529 ! self::is_admin_post()
15530 );
15531 }
15532
15533 /**
15534 * Check if a real user is in the customizer view.
15535 *
15536 * @author Vova Feldman (@svovaf)
15537 * @since 1.2.2.7
15538 *
15539 * @return bool
15540 */
15541 static function is_customizer() {
15542 return is_customize_preview();
15543 }
15544
15545 /**
15546 * Check if running in HTTPS and if site's plan matching the specified plan.
15547 *
15548 * @param string $plan
15549 * @param bool $exact
15550 *
15551 * @return bool
15552 */
15553 function is_ssl_and_plan( $plan, $exact = false ) {
15554 return ( $this->is_ssl() && $this->is_plan( $plan, $exact ) );
15555 }
15556
15557 /**
15558 * Construct plugin's settings page URL.
15559 *
15560 * @author Vova Feldman (@svovaf)
15561 * @since 1.0.4
15562 *
15563 * @param string $page
15564 * @param array $params
15565 * @param bool|null $network
15566 *
15567 * @return string
15568 */
15569 function _get_admin_page_url( $page = '', $params = array(), $network = null ) {
15570 if ( is_null( $network ) ) {
15571 $network = (
15572 $this->_is_network_active &&
15573 ( fs_is_network_admin() || ! $this->is_delegated_connection() )
15574 );
15575 }
15576
15577 if ( 0 < count( $params ) ) {
15578 foreach ( $params as $k => $v ) {
15579 $params[ $k ] = urlencode( $v );
15580 }
15581 }
15582
15583 $page_param = $this->_menu->get_slug( $page );
15584
15585 if ( empty( $page ) &&
15586 // Show the opt-in as an overlay for free wp.org themes or themes without any settings page.
15587 $this->show_opt_in_on_themes_page()
15588 ) {
15589 $params[ $this->get_unique_affix() . '_show_optin' ] = 'true';
15590
15591 return add_query_arg(
15592 $params,
15593 $this->admin_url( 'themes.php', 'admin', $network )
15594 );
15595 }
15596
15597 if ( ! $this->has_settings_menu() ) {
15598 if ( ! empty( $page ) ) {
15599 // Module doesn't have a setting page, but since the request is for
15600 // a specific Freemius page, use the admin.php path.
15601 return add_query_arg( array_merge( $params, array(
15602 'page' => $page_param,
15603 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15604 } else {
15605 if ( $this->is_activation_mode() ) {
15606 /**
15607 * @author Vova Feldman
15608 * @since 1.2.1.6
15609 *
15610 * If plugin doesn't have a settings page, create one for the opt-in screen.
15611 */
15612 return add_query_arg( array_merge( $params, array(
15613 'page' => $this->_slug,
15614 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15615 } else {
15616 // Plugin without a settings page.
15617 return add_query_arg(
15618 $params,
15619 $this->admin_url( 'plugins.php', 'admin', $network )
15620 );
15621 }
15622 }
15623 }
15624
15625 // Module has a submenu settings page.
15626 if ( ! $this->_menu->is_top_level() ) {
15627 $parent_slug = $this->_menu->get_parent_slug();
15628 $menu_file = ( false !== strpos( $parent_slug, '.php' ) ) ?
15629 $parent_slug :
15630 'admin.php';
15631
15632 return add_query_arg( array_merge( $params, array(
15633 'page' => $page_param,
15634 ) ), $this->admin_url( $menu_file, 'admin', $network ) );
15635 }
15636
15637 // Module has a top level CPT settings page.
15638 if ( $this->_menu->is_cpt() ) {
15639 if ( empty( $page ) && $this->is_activation_mode() ) {
15640 return add_query_arg( array_merge( $params, array(
15641 'page' => $page_param
15642 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15643 } else {
15644 if ( ! empty( $page ) ) {
15645 $params['page'] = $page_param;
15646 }
15647
15648 return add_query_arg(
15649 $params,
15650 $this->admin_url( $this->_menu->get_raw_slug(), 'admin', $network )
15651 );
15652 }
15653 }
15654
15655 // Module has a custom top level settings page.
15656 return add_query_arg( array_merge( $params, array(
15657 'page' => $page_param,
15658 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
15659 }
15660
15661 #--------------------------------------------------------------------------------
15662 #region Multisite
15663 #--------------------------------------------------------------------------------
15664
15665 /**
15666 * @author Leo Fajardo (@leorw)
15667 * @since 2.0.0
15668 *
15669 * @return bool
15670 */
15671 function is_network_active() {
15672 return $this->_is_network_active;
15673 }
15674
15675 /**
15676 * Delegate activation for the given sites in the network (or all sites if `null`) to site admins.
15677 *
15678 * @author Leo Fajardo (@leorw)
15679 * @since 2.0.0
15680 *
15681 * @param bool|int[] $all_or_blog_ids
15682 */
15683 private function delegate_connection( $all_or_blog_ids = true ) {
15684 $this->_logger->entrance();
15685
15686 $this->_admin_notices->remove_sticky( 'connect_account' );
15687
15688 if ( true === $all_or_blog_ids ) {
15689 // All sites delegation.
15690 $this->_storage->store( 'is_delegated_connection', true, true );
15691 } else {
15692 // Specified sites delegation.
15693 foreach ( $all_or_blog_ids as $blog_id ) {
15694 $this->delegate_site_connection( $blog_id );
15695 }
15696 }
15697
15698 $this->network_upgrade_mode_completed();
15699 }
15700
15701 /**
15702 * Delegate specific network site conncetion to the site admin.
15703 *
15704 * @author Vova Feldman (@svovaf)
15705 * @since 2.0.0
15706 *
15707 * @param int $blog_id
15708 */
15709 private function delegate_site_connection( $blog_id ) {
15710 $this->_storage->store( 'is_delegated_connection', true, $blog_id );
15711 }
15712
15713 /**
15714 * Check if super-admin delegated the connection of ALL sites to the site admins.
15715 *
15716 * @author Vova Feldman (@svovaf)
15717 * @since 2.0.0
15718 *
15719 * @return bool
15720 */
15721 function is_network_delegated_connection() {
15722 if ( ! $this->_is_network_active ) {
15723 return false;
15724 }
15725
15726 return $this->_storage->get( 'is_delegated_connection', false, true );
15727 }
15728
15729 /**
15730 * @author Leo Fajardo (@leorw)
15731 * @since 2.0.0
15732 *
15733 * @param int $blog_id
15734 *
15735 * @return bool
15736 */
15737 function is_site_delegated_connection( $blog_id = 0 ) {
15738 if ( ! $this->_is_network_active ) {
15739 return false;
15740 }
15741
15742 if ( 0 == $blog_id ) {
15743 $blog_id = get_current_blog_id();
15744 }
15745
15746 return $this->_storage->get( 'is_delegated_connection', false, $blog_id );
15747 }
15748
15749 /**
15750 * Check if delegated the connection. When running within the network admin,
15751 * and haven't specified the blog ID, checks if network level delegated. If running
15752 * within a site admin or specified a blog ID, check if delegated the connection for
15753 * the current context site.
15754 *
15755 * If executed outside the the admin, check if delegated the connection
15756 * for the current context site OR the whole network.
15757 *
15758 * @author Vova Feldman (@svovaf)
15759 * @since 2.0.0
15760 *
15761 * @param int $blog_id If set, checks if network delegated or blog specific delegated.
15762 *
15763 * @return bool
15764 */
15765 function is_delegated_connection( $blog_id = 0 ) {
15766 if ( ! $this->_is_network_active ) {
15767 return false;
15768 }
15769
15770 if ( fs_is_network_admin() && 0 == $blog_id ) {
15771 return $this->is_network_delegated_connection();
15772 }
15773
15774 return (
15775 $this->is_network_delegated_connection() ||
15776 $this->is_site_delegated_connection( $blog_id )
15777 );
15778 }
15779
15780 /**
15781 * Check if the current module is active for the site.
15782 *
15783 * @author Vova Feldman (@svovaf)
15784 * @since 2.0.0
15785 *
15786 * @param int $blog_id
15787 *
15788 * @return bool
15789 */
15790 function is_active_for_site( $blog_id ) {
15791 if ( ! is_multisite() ) {
15792 // Not a multisite and this code is executed, means that the plugin is active.
15793 return true;
15794 }
15795
15796 if ( $this->is_theme() ) {
15797 // All themes are site level activated.
15798 return true;
15799 }
15800
15801 if ( $this->_is_network_active ) {
15802 // Plugin was network activated so it's active.
15803 return true;
15804 }
15805
15806 return in_array( $this->_plugin_basename, (array) get_blog_option( $blog_id, 'active_plugins', array() ) );
15807 }
15808
15809 /**
15810 * @todo Implement pagination when accessing the subsites collection.
15811 *
15812 * @author Leo Fajardo (@leorw)
15813 * @since 2.0.0
15814 *
15815 * @param int $limit Default to 1,000
15816 * @param int $offset Default to 0
15817 *
15818 * @return array Active & public sites collection.
15819 */
15820 static function get_sites( $limit = 1000, $offset = 0 ) {
15821 if ( ! is_multisite() ) {
15822 return array();
15823 }
15824
15825 /**
15826 * For consistency with get_blog_list() which only return active public sites.
15827 *
15828 * @author Vova Feldman (@svovaf)
15829 */
15830 $args = array(
15831 /**
15832 * Commented out in order to handle the migration of site options whether the site is public or not.
15833 *
15834 * @author Leo Fajardo (@leorw)
15835 * @since 2.2.1
15836 */
15837 // 'public' => 1,
15838 'archived' => 0,
15839 'mature' => 0,
15840 'spam' => 0,
15841 'deleted' => 0,
15842 'number' => $limit,
15843 'offset' => $offset,
15844 );
15845
15846 return get_sites( $args );
15847 }
15848
15849 /**
15850 * Checks if a given blog is active.
15851 *
15852 * @author Vova Feldman (@svovaf)
15853 * @since 2.0.0
15854 *
15855 * @param $blog_id
15856 *
15857 * @return bool
15858 */
15859 private static function is_site_active( $blog_id ) {
15860 global $wpdb;
15861
15862 $blog_info = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->blogs} WHERE blog_id = %d", $blog_id ) );
15863
15864 if ( ! is_object( $blog_info ) ) {
15865 return false;
15866 }
15867
15868 return (
15869 true == $blog_info->public &&
15870 false == $blog_info->archived &&
15871 false == $blog_info->mature &&
15872 false == $blog_info->spam &&
15873 false == $blog_info->deleted
15874 );
15875 }
15876
15877 /**
15878 * Get a mapping between the site addresses to their blog IDs.
15879 *
15880 * @author Vova Feldman (@svovaf)
15881 * @since 2.0.0
15882 *
15883 * @return array {
15884 * @key string Site address without protocol with a trailing slash.
15885 * @value int Site's blog ID.
15886 * }
15887 */
15888 private function get_address_to_blog_map() {
15889 $sites = self::get_sites();
15890
15891 // Map site addresses to their blog IDs.
15892 $address_to_blog_map = array();
15893 foreach ( $sites as $site ) {
15894 $blog_id = self::get_site_blog_id( $site );
15895 $address = self::get_unfiltered_site_url( $blog_id, true, true );
15896 $address_to_blog_map[ $address ] = $blog_id;
15897 }
15898
15899 return $address_to_blog_map;
15900 }
15901
15902 /**
15903 * Get a mapping between the site addresses to their blog IDs.
15904 *
15905 * @author Vova Feldman (@svovaf)
15906 * @since 2.0.0
15907 *
15908 * @return array {
15909 * @key int Site's blog ID.
15910 * @value FS_Site Associated install.
15911 * }
15912 */
15913 function get_blog_install_map() {
15914 $sites = self::get_sites();
15915
15916 // Map site blog ID to its install.
15917 $install_map = array();
15918
15919 foreach ( $sites as $site ) {
15920 $blog_id = self::get_site_blog_id( $site );
15921 $install = $this->get_install_by_blog_id( $blog_id );
15922
15923 if ( is_object( $install ) ) {
15924 $install_map[ $blog_id ] = $install;
15925 }
15926 }
15927
15928 return $install_map;
15929 }
15930
15931 /**
15932 * @author Vova Feldman (@svovaf)
15933 * @since 2.5.1
15934 *
15935 * @param bool|null $is_delegated When `true`, returns only connection delegated blog IDs. When `false`, only non-delegated blog IDs.
15936 *
15937 * @return int[]
15938 */
15939 private function get_blog_ids( $is_delegated = null ) {
15940 $blog_ids = array();
15941
15942 $sites = self::get_sites();
15943 foreach ( $sites as $site ) {
15944 $blog_id = self::get_site_blog_id( $site );
15945
15946 if (
15947 is_null( $is_delegated ) ||
15948 $is_delegated === $this->is_site_delegated_connection( $blog_id )
15949 ) {
15950 $blog_ids[] = $blog_id;
15951 }
15952 }
15953
15954 return $blog_ids;
15955 }
15956
15957 /**
15958 * @author Vova Feldman (@svovaf)
15959 * @since 2.5.1
15960 *
15961 * @return int[]
15962 */
15963 private function get_non_delegated_blog_ids() {
15964 return $this->get_blog_ids( false );
15965 }
15966
15967 /**
15968 * Gets a map of module IDs that the given user has opted-in to.
15969 *
15970 * @author Leo Fajardo (@leorw)
15971 * @since 2.1.0
15972 *
15973 * @param number $fs_user_id
15974 *
15975 * @return array {
15976 * @key number $plugin_id
15977 * @value bool Always true.
15978 * }
15979 */
15980 private static function get_user_opted_in_module_ids_map( $fs_user_id ) {
15981 self::$_static_logger->entrance();
15982
15983 if ( ! is_multisite() ) {
15984 $installs = array_merge(
15985 self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN ),
15986 self::get_all_sites( WP_FS__MODULE_TYPE_THEME )
15987 );
15988 } else {
15989 $sites = self::get_sites();
15990
15991 $installs = array();
15992 foreach ( $sites as $site ) {
15993 $blog_id = self::get_site_blog_id( $site );
15994
15995 $installs = array_merge(
15996 $installs,
15997 self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN, $blog_id ),
15998 self::get_all_sites( WP_FS__MODULE_TYPE_THEME, $blog_id )
15999 );
16000 }
16001 }
16002
16003 $module_ids_map = array();
16004 foreach ( $installs as $install ) {
16005 if ( is_object( $install ) &&
16006 FS_Site::is_valid_id( $install->id ) &&
16007 FS_User::is_valid_id( $install->user_id ) &&
16008 ( $install->user_id == $fs_user_id )
16009 ) {
16010 $module_ids_map[ $install->plugin_id ] = true;
16011 }
16012 }
16013
16014 return $module_ids_map;
16015 }
16016
16017 /**
16018 * @author Leo Fajardo (@leorw)
16019 *
16020 * @return null|array {
16021 * 'install' => FS_Site Module's install,
16022 * 'blog_id' => string The associated blog ID.
16023 * }
16024 */
16025 function find_first_install() {
16026 $sites = self::get_sites();
16027
16028 foreach ( $sites as $site ) {
16029 $blog_id = self::get_site_blog_id( $site );
16030 $install = $this->get_install_by_blog_id( $blog_id );
16031
16032 if ( is_object( $install ) ) {
16033 return array(
16034 'install' => $install,
16035 'blog_id' => $blog_id
16036 );
16037 }
16038 }
16039
16040 return null;
16041 }
16042
16043 /**
16044 * Switches the Freemius site level context to a specified blog.
16045 *
16046 * @author Vova Feldman (@svovaf)
16047 * @since 2.0.0
16048 *
16049 * @param int $blog_id
16050 * @param FS_Site $install
16051 * @param bool $flush
16052 *
16053 * @return bool Since 2.3.1 returns if a switch was made.
16054 */
16055 function switch_to_blog( $blog_id, FS_Site $install = null, $flush = false ) {
16056 if ( ! is_numeric( $blog_id ) ) {
16057 return false;
16058 }
16059
16060 if ( ! $flush && $blog_id == $this->_context_is_network_or_blog_id ) {
16061 return false;
16062 }
16063
16064 switch_to_blog( $blog_id );
16065 $this->_context_is_network_or_blog_id = $blog_id;
16066
16067 self::$_accounts->set_site_blog_context( $blog_id );
16068 $this->_storage->set_site_blog_context( $blog_id );
16069 $this->_storage->set_network_active( $this->_is_network_active, $this->is_delegated_connection( $blog_id ) );
16070
16071 $this->_site = is_object( $install ) ?
16072 $install :
16073 $this->get_install_by_blog_id( $blog_id );
16074
16075 $this->_user = false;
16076 $this->_licenses = false;
16077 $this->_license = null;
16078 $this->is_whitelabeled = null;
16079
16080 if ( is_object( $this->_site ) ) {
16081 // Try to fetch user from install.
16082 $this->_user = self::_get_user_by_id( $this->_site->user_id );
16083
16084 if ( ! is_object( $this->_user ) &&
16085 FS_User::is_valid_id( $this->_storage->prev_user_id )
16086 ) {
16087 // Try to fetch previously saved user.
16088 $this->_user = self::_get_user_by_id( $this->_storage->prev_user_id );
16089
16090 if ( ! is_object( $this->_user ) ) {
16091 // Fallback to network's user.
16092 $this->_user = $this->get_network_user();
16093 }
16094 }
16095
16096 $all_plugin_licenses = self::get_all_licenses( $this->_module_id );
16097
16098 if ( ! empty( $all_plugin_licenses ) ) {
16099 if ( ! FS_Plugin_License::is_valid_id( $this->_site->license_id ) ) {
16100 $this->_license = null;
16101 } else {
16102 $license_found = false;
16103 foreach ( $all_plugin_licenses as $license ) {
16104 if ( $license->id == $this->_site->license_id ) {
16105 // License found.
16106 $this->_license = $license;
16107 $license_found = true;
16108 break;
16109 }
16110 }
16111
16112 if ( $license_found ) {
16113 $this->link_license_2_user( $this->_license->id, $this->_user->id );
16114 }
16115 }
16116
16117 $this->_licenses = $this->get_user_licenses( $this->_user->id );
16118 }
16119 }
16120
16121 unset( $this->_site_api );
16122 unset( $this->_user_api );
16123
16124 return true;
16125 }
16126
16127 /**
16128 * Restore the blog context to the blog that originally loaded the module.
16129 *
16130 * @author Vova Feldman (@svovaf)
16131 * @since 2.0.0
16132 */
16133 function restore_current_blog() {
16134 $this->switch_to_blog( $this->_blog_id );
16135 }
16136
16137 /**
16138 * @author Vova Feldman (@svovaf)
16139 * @since 2.0.0
16140 *
16141 * @param array|WP_Site $site
16142 *
16143 * @return int
16144 */
16145 static function get_site_blog_id( &$site ) {
16146 return ( $site instanceof WP_Site ) ?
16147 $site->blog_id :
16148 ( is_object( $site ) && isset( $site->userblog_id ) ?
16149 $site->userblog_id :
16150 $site['blog_id'] );
16151 }
16152
16153 /**
16154 * @author Vova Feldman (@svovaf)
16155 * @since 2.5.1
16156 *
16157 * @param WP_Site[]|array[] $sites
16158 *
16159 * @return int[]
16160 */
16161 static function get_sites_blog_ids( $sites ) {
16162 $blog_ids = array();
16163 foreach ( $sites as $site ) {
16164 $blog_ids[] = self::get_site_blog_id( $site );
16165 }
16166
16167 return $blog_ids;
16168 }
16169
16170 /**
16171 * @author Leo Fajardo (@leorw)
16172 * @since 2.0.0
16173 *
16174 * @param array|WP_Site|null $site
16175 * @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.
16176 *
16177 * @return array
16178 */
16179 function get_site_info( $site = null, $load_registration = false ) {
16180 $this->_logger->entrance();
16181
16182 $switched = false;
16183
16184 $registration_date = null;
16185
16186 if ( is_null( $site ) ) {
16187 $url = self::get_unfiltered_site_url();
16188 $name = get_bloginfo( 'name' );
16189 $blog_id = null;
16190 } else {
16191 $blog_id = self::get_site_blog_id( $site );
16192
16193 if ( get_current_blog_id() != $blog_id ) {
16194 switch_to_blog( $blog_id );
16195 $switched = true;
16196 }
16197
16198 if ( $site instanceof WP_Site ) {
16199 $url = $site->siteurl;
16200 $name = $site->blogname;
16201 $registration_date = $site->registered;
16202 } else {
16203 $url = self::get_unfiltered_site_url( $blog_id );
16204 $name = get_bloginfo( 'name' );
16205 }
16206 }
16207
16208 if ( empty( $registration_date ) && $load_registration ) {
16209 $blog_details = get_blog_details( $blog_id, false );
16210
16211 if ( is_object( $blog_details ) && isset( $blog_details->registered ) ) {
16212 $registration_date = $blog_details->registered;
16213 }
16214 }
16215
16216 $info = array(
16217 'uid' => $this->get_anonymous_id( $blog_id ),
16218 'url' => $url,
16219 );
16220
16221 // Add these diagnostic information only if user allowed to track.
16222 if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) {
16223 $info = array_merge( $info, array(
16224 'title' => $name,
16225 'language' => self::get_sanitized_language(),
16226 ) );
16227 }
16228
16229 if ( is_numeric( $blog_id ) ) {
16230 $info['blog_id'] = $blog_id;
16231 }
16232
16233 if ( ! empty( $registration_date ) ) {
16234 $info[ 'registration_date' ] = $registration_date;
16235 }
16236
16237 if ( $switched ) {
16238 restore_current_blog();
16239 }
16240
16241 return $info;
16242 }
16243
16244 /**
16245 * Load the module's install based on the blog ID.
16246 *
16247 * @author Vova Feldman (@svovaf)
16248 * @since 2.0.0
16249 *
16250 * @param int|null $blog_id
16251 *
16252 * @return FS_Site
16253 */
16254 function get_install_by_blog_id( $blog_id = null ) {
16255 $installs = self::get_all_sites( $this->_module_type, $blog_id );
16256 $install = isset( $installs[ $this->_slug ] ) ? $installs[ $this->_slug ] : null;
16257
16258 if ( is_object( $install ) &&
16259 is_numeric( $install->id ) &&
16260 is_numeric( $install->user_id ) &&
16261 FS_Plugin_Plan::is_valid_id( $install->plan_id )
16262 ) {
16263 // Load site.
16264 $install = clone $install;
16265 }
16266
16267 return $install;
16268 }
16269
16270 /**
16271 * Check if module is installed on a specified site.
16272 *
16273 * @author Vova Feldman (@svovaf)
16274 * @since 2.0.0
16275 *
16276 * @param int|null $blog_id
16277 *
16278 * @return bool
16279 */
16280 function is_installed_on_site( $blog_id = null ) {
16281 $installs = self::get_all_sites( $this->_module_type, $blog_id );
16282 $install = isset( $installs[ $this->_slug ] ) ? $installs[ $this->_slug ] : null;
16283
16284 return (
16285 is_object( $install ) &&
16286 is_numeric( $install->id ) &&
16287 is_numeric( $install->user_id ) &&
16288 FS_Plugin_Plan::is_valid_id( $install->plan_id )
16289 );
16290 }
16291
16292 /**
16293 * Check if super-admin connected at least one site via the network opt-in.
16294 *
16295 * @author Vova Feldman (@svovaf)
16296 * @since 2.0.0
16297 *
16298 * @return bool
16299 */
16300 function is_network_registered() {
16301 if ( ! $this->_is_network_active ) {
16302 return false;
16303 }
16304
16305 return FS_User::is_valid_id( $this->_storage->network_user_id );
16306 }
16307
16308 /**
16309 * Returns the main user associated with the network.
16310 *
16311 * @author Vova Feldman (@svovaf)
16312 * @since 2.0.0
16313 *
16314 * @return FS_User
16315 */
16316 function get_network_user() {
16317 if ( ! $this->_is_network_active ) {
16318 return null;
16319 }
16320
16321 return FS_User::is_valid_id( $this->_storage->network_user_id ) ?
16322 self::_get_user_by_id( $this->_storage->network_user_id ) :
16323 null;
16324 }
16325
16326 /**
16327 * Returns the current context user or the network's main user.
16328 *
16329 * @author Vova Feldman (@svovaf)
16330 * @since 2.0.0
16331 *
16332 * @return FS_User
16333 */
16334 function get_current_or_network_user() {
16335 return ( $this->_user instanceof FS_User ) ?
16336 $this->_user :
16337 $this->get_network_user();
16338 }
16339
16340 /**
16341 * Returns the main install associated with the network.
16342 *
16343 * @author Vova Feldman (@svovaf)
16344 * @since 2.0.0
16345 *
16346 * @return FS_Site
16347 */
16348 function get_network_install() {
16349 if ( ! $this->_is_network_active ) {
16350 return null;
16351 }
16352
16353 return FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ?
16354 $this->get_install_by_blog_id( $this->_storage->network_install_blog_id ) :
16355 null;
16356 }
16357
16358 /**
16359 * Returns the blog ID that is associated with the main install.
16360 *
16361 * @author Leo Fajardo (@leorw)
16362 * @since 2.0.0
16363 *
16364 * @return int|null
16365 */
16366 function get_network_install_blog_id() {
16367 if ( ! $this->_is_network_active ) {
16368 return null;
16369 }
16370
16371 return FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ?
16372 $this->_storage->network_install_blog_id :
16373 null;
16374 }
16375
16376 /**
16377 * Returns the current context install or the network's main install.
16378 *
16379 * @author Vova Feldman (@svovaf)
16380 * @since 2.0.0
16381 *
16382 * @return FS_Site
16383 */
16384 function get_current_or_network_install() {
16385 return ( $this->_site instanceof FS_Site ) ?
16386 $this->_site :
16387 $this->get_network_install();
16388 }
16389
16390 /**
16391 * Check if executing a site level action from the network level admin.
16392 *
16393 * @author Vova Feldman (@svovaf)
16394 * @since 2.0.0
16395 *
16396 * @return false|int If yes, return the requested blog ID.
16397 */
16398 private function is_network_level_site_specific_action() {
16399 if ( ! $this->_is_network_active ) {
16400 return false;
16401 }
16402
16403 if ( ! fs_is_network_admin() ) {
16404 return false;
16405 }
16406
16407 $blog_id = fs_request_get( 'blog_id', '' );
16408
16409 return is_numeric( $blog_id ) ? $blog_id : false;
16410 }
16411
16412 /**
16413 * Check if executing an action from the network level admin.
16414 *
16415 * @author Vova Feldman (@svovaf)
16416 * @since 2.0.0
16417 *
16418 * @return bool
16419 */
16420 private function is_network_level_action() {
16421 return ( $this->_is_network_active && fs_is_network_admin() );
16422 }
16423
16424 /**
16425 * Needs to be executed after site deactivation, archive, deletion, or flag as spam.
16426 * 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.
16427 *
16428 * @author Vova Feldman (@svovaf)
16429 * @since 2.0.0
16430 *
16431 * @param int $context_blog_id
16432 */
16433 private function update_multisite_data_after_site_deactivation( $context_blog_id = 0 ) {
16434 $this->_logger->entrance();
16435
16436 if ( $this->_is_network_active ) {
16437 if ( $context_blog_id == $this->_storage->network_install_blog_id ) {
16438 $installs_map = $this->get_blog_install_map();
16439
16440 foreach ( $installs_map as $blog_id => $install ) {
16441 /**
16442 * @var FS_Site $install
16443 */
16444 if ( $context_blog_id == $blog_id ) {
16445 continue;
16446 }
16447
16448 if ( $install->user_id != $this->_storage->network_user_id ) {
16449 continue;
16450 }
16451
16452 // Switch reference to a blog that is opted-in and belong to the same super-admin.
16453 $this->_storage->network_install_blog_id = $blog_id;
16454 break;
16455 }
16456 }
16457 }
16458
16459 if ( ! $this->is_registered() ) {
16460 return;
16461 }
16462
16463 if ( $this->is_sync_cron_scheduled() &&
16464 $context_blog_id == $this->get_sync_cron_blog_id()
16465 ) {
16466 $this->schedule_sync_cron( WP_FS__SCRIPT_START_TIME, true, $context_blog_id );
16467 }
16468
16469 if ( $this->is_install_sync_scheduled() &&
16470 $context_blog_id == $this->get_install_sync_cron_blog_id()
16471 ) {
16472 $this->schedule_install_sync( $context_blog_id );
16473 }
16474 }
16475
16476 /**
16477 * Executed after site deactivation, archive, or flag as spam.
16478 *
16479 * @author Vova Feldman (@svovaf)
16480 * @since 2.0.0
16481 *
16482 * @param int $context_blog_id
16483 */
16484 public function _after_site_deactivated_callback( $context_blog_id = 0 ) {
16485 $this->_logger->entrance();
16486
16487 $install = $this->get_install_by_blog_id( $context_blog_id );
16488
16489 if ( ! is_object( $install ) ) {
16490 // Site not connected.
16491 return;
16492 }
16493
16494 $this->update_multisite_data_after_site_deactivation( $context_blog_id );
16495
16496 if ( ! $this->is_registered() ) {
16497 return;
16498 }
16499
16500 $current_blog_id = get_current_blog_id();
16501
16502 $this->switch_to_blog( $context_blog_id );
16503
16504 // Send deactivation event.
16505 $this->sync_install( array(
16506 'is_active' => false,
16507 ) );
16508
16509 $this->switch_to_blog( $current_blog_id );
16510 }
16511
16512 /**
16513 * Executed after site deletion.
16514 *
16515 * @author Vova Feldman (@svovaf)
16516 * @since 2.0.0
16517 *
16518 * @param int $context_blog_id
16519 * @param bool $drop True if site's database tables should be dropped. Default is false.
16520 */
16521 public function _after_site_deleted_callback( $context_blog_id = 0, $drop = false ) {
16522 $this->_logger->entrance();
16523
16524 $install = $this->get_install_by_blog_id( $context_blog_id );
16525
16526 if ( ! is_object( $install ) ) {
16527 // Site not connected.
16528 return;
16529 }
16530
16531 $this->update_multisite_data_after_site_deactivation( $context_blog_id );
16532
16533 if ( ! $this->is_registered() ) {
16534 return;
16535 }
16536
16537 $current_blog_id = get_current_blog_id();
16538
16539 $this->switch_to_blog( $context_blog_id );
16540
16541 if ( $drop ) {
16542 // Delete install if dropping site DB.
16543 $this->delete_account_event();
16544 } else {
16545 // Send deactivation event.
16546 $this->sync_install( array(
16547 'is_active' => false,
16548 ) );
16549 }
16550
16551 $this->switch_to_blog( $current_blog_id );
16552 }
16553
16554 /**
16555 * Executed after site deletion, called from wp_delete_site
16556 *
16557 * @author Dario Curvino (@dudo)
16558 * @since 2.5.0
16559 *
16560 * @param WP_Site $old_site
16561 */
16562 public function _after_wpsite_deleted_callback( WP_Site $old_site ) {
16563 $this->_logger->entrance();
16564
16565 $this->_after_site_deleted_callback( $old_site->blog_id, true );
16566 }
16567
16568 /**
16569 * Executed after site re-activation.
16570 *
16571 * @author Vova Feldman (@svovaf)
16572 * @since 2.0.0
16573 *
16574 * @param int $context_blog_id
16575 */
16576 public function _after_site_reactivated_callback( $context_blog_id = 0 ) {
16577 $this->_logger->entrance();
16578
16579 $install = $this->get_install_by_blog_id( $context_blog_id );
16580
16581 if ( ! is_object( $install ) ) {
16582 // Site not connected.
16583 return;
16584 }
16585
16586 if ( ! self::is_site_active( $context_blog_id ) ) {
16587 // Site not yet active (can be in spam mode, archived, deleted...).
16588 return;
16589 }
16590
16591 $current_blog_id = get_current_blog_id();
16592
16593 $this->switch_to_blog( $context_blog_id );
16594
16595 // Send re-activation event.
16596 $this->sync_install( array(
16597 'is_active' => true,
16598 ) );
16599
16600 $this->switch_to_blog( $current_blog_id );
16601 }
16602
16603 #endregion Multisite
16604
16605 /**
16606 * @author Leo Fajardo (@leorw)
16607 *
16608 * @param string $path
16609 * @param string $scheme
16610 * @param bool $network
16611 *
16612 * @return string
16613 */
16614 private function admin_url( $path = '', $scheme = 'admin', $network = true ) {
16615 return ( $this->_is_network_active && $network ) ?
16616 network_admin_url( $path, $scheme ) :
16617 admin_url( $path, $scheme );
16618 }
16619
16620 /**
16621 * Check if currently in a specified admin page.
16622 *
16623 * @author Vova Feldman (@svovaf)
16624 * @since 1.2.2.7
16625 *
16626 * @param string $page
16627 *
16628 * @return bool
16629 */
16630 function is_admin_page( $page ) {
16631 return ( $this->_menu->get_slug( $page ) === fs_request_get( 'page', '', 'get' ) );
16632 }
16633
16634 /**
16635 * Check if currently in the product's main admin page.
16636 *
16637 * @author Vova Feldman (@svovaf)
16638 * @since 2.3.1
16639 *
16640 * @return bool
16641 */
16642 function is_main_admin_page() {
16643 return $this->is_admin_page( '' );
16644 }
16645
16646 /**
16647 * Get module's main admin setting page URL.
16648 *
16649 * @author Vova Feldman (@svovaf)
16650 * @since 1.2.2.7
16651 *
16652 * @return string
16653 */
16654 function main_menu_url() {
16655 return $this->_menu->main_menu_url();
16656 }
16657
16658 /**
16659 * Check if currently on the theme's setting page or
16660 * on any of the Freemius added pages (via tabs).
16661 *
16662 * @author Vova Feldman (@svovaf)
16663 * @since 1.2.2.7
16664 *
16665 * @return bool
16666 *
16667 * @deprecated Please use is_product_settings_page() instead;
16668 */
16669 function is_theme_settings_page() {
16670 return $this->is_product_settings_page();
16671 }
16672
16673 /**
16674 * Check if currently on the product's main setting page or on any of the Freemius added pages (via tabs).
16675 *
16676 * @author Vova Feldman (@svovaf)
16677 * @since 1.2.2.7
16678 *
16679 * @return bool
16680 */
16681 function is_product_settings_page() {
16682 $page = fs_request_get( 'page', '', 'get' );
16683 $menu_slug = $this->_menu->get_slug();
16684
16685 if ( $page === $menu_slug ) {
16686 return true;
16687 }
16688
16689 return fs_starts_with(
16690 // e.g., {$menu_slug}-account, {$menu_slug}-affiliation, etc.
16691 $page,
16692 ( $menu_slug . '-' )
16693 );
16694 }
16695
16696 /**
16697 * Plugin's account page + sync license URL.
16698 *
16699 * @author Vova Feldman (@svovaf)
16700 * @since 1.1.9.1
16701 *
16702 * @param bool|number $plugin_id
16703 * @param bool $add_action_nonce
16704 * @param array $params
16705 *
16706 * @return string
16707 */
16708 function _get_sync_license_url( $plugin_id = false, $add_action_nonce = true, $params = array() ) {
16709 if ( is_numeric( $plugin_id ) ) {
16710 $params['plugin_id'] = $plugin_id;
16711 }
16712
16713 return $this->get_account_url(
16714 $this->get_unique_affix() . '_sync_license',
16715 $params,
16716 $add_action_nonce
16717 );
16718 }
16719
16720 /**
16721 * Plugin's account URL.
16722 *
16723 * @author Vova Feldman (@svovaf)
16724 * @since 1.0.4
16725 *
16726 * @param bool|string $action
16727 * @param array $params
16728 *
16729 * @param bool $add_action_nonce
16730 *
16731 * @return string
16732 */
16733 function get_account_url( $action = false, $params = array(), $add_action_nonce = true ) {
16734 if ( is_string( $action ) ) {
16735 $params['fs_action'] = $action;
16736 }
16737
16738 self::require_pluggable_essentials();
16739
16740 return ( $add_action_nonce && is_string( $action ) ) ?
16741 fs_nonce_url( $this->_get_admin_page_url( 'account', $params ), $action ) :
16742 $this->_get_admin_page_url( 'account', $params );
16743 }
16744
16745 /**
16746 * @author Vova Feldman (@svovaf)
16747 * @since 1.2.0
16748 *
16749 * @param string $tab
16750 * @param bool $action
16751 * @param array $params
16752 * @param bool $add_action_nonce
16753 *
16754 * @return string
16755 *
16756 * @uses get_account_url()
16757 */
16758 function get_account_tab_url( $tab, $action = false, $params = array(), $add_action_nonce = true ) {
16759 $params['tab'] = $tab;
16760
16761 return $this->get_account_url( $action, $params, $add_action_nonce );
16762 }
16763
16764 /**
16765 * Plugin's account URL.
16766 *
16767 * @author Vova Feldman (@svovaf)
16768 * @since 1.0.4
16769 *
16770 * @param bool|string $topic
16771 * @param bool|string $message
16772 * @param bool|string $summary Since 2.5.1.
16773 *
16774 * @return string
16775 */
16776 function contact_url( $topic = false, $message = false, $summary = false ) {
16777 $params = array();
16778 if ( is_string( $topic ) ) {
16779 $params['topic'] = $topic;
16780 }
16781 if ( is_string( $message ) ) {
16782 $params['message'] = $message;
16783 }
16784
16785 if ( is_string( $summary ) ) {
16786 $params['summary'] = $summary;
16787 }
16788
16789 if ( $this->is_addon() ) {
16790 $params['addon_id'] = $this->get_id();
16791
16792 return $this->get_parent_instance()->_get_admin_page_url( 'contact', $params );
16793 } else {
16794 return $this->_get_admin_page_url( 'contact', $params );
16795 }
16796 }
16797
16798 /**
16799 * Add-on direct info URL.
16800 *
16801 * @author Vova Feldman (@svovaf)
16802 * @since 1.1.0
16803 *
16804 * @param string $slug
16805 *
16806 * @return string
16807 */
16808 function addon_url( $slug ) {
16809 return $this->_get_admin_page_url( 'addons', array(
16810 'slug' => $slug
16811 ) );
16812 }
16813
16814 /**
16815 * Add-ons URL.
16816 *
16817 * @author Vova Feldman (@svovaf)
16818 * @since 2.4.5
16819 *
16820 * @return string
16821 */
16822 function get_addons_url() {
16823 return $this->_get_admin_page_url( 'addons' );
16824 }
16825
16826 /* Logger
16827 ------------------------------------------------------------------------------------------------------------------*/
16828 /**
16829 * @param string $id
16830 * @param bool $prefix_slug
16831 *
16832 * @return FS_Logger
16833 */
16834 function get_logger( $id = '', $prefix_slug = true ) {
16835 return FS_Logger::get_logger( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id );
16836 }
16837
16838 /**
16839 * Note: This method is used externally so don't delete it.
16840 *
16841 * @param $id
16842 * @param bool $load_options
16843 * @param bool $prefix_slug
16844 *
16845 * @return FS_Option_Manager
16846 */
16847 function get_options_manager( $id, $load_options = false, $prefix_slug = true ) {
16848 return FS_Option_Manager::get_manager( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id, $load_options );
16849 }
16850
16851 /* Security
16852 ------------------------------------------------------------------------------------------------------------------*/
16853 private static function _encrypt( $str ) {
16854 if ( is_null( $str ) ) {
16855 return null;
16856 }
16857
16858 /**
16859 * The encrypt/decrypt functions are used to protect
16860 * the user from messing up with some of the sensitive
16861 * data stored for the module as a JSON in the database.
16862 *
16863 * I used the same suggested hack by the theme review team.
16864 * For more details, look at the function `Base64UrlDecode()`
16865 * in `./sdk/FreemiusBase.php`.
16866 *
16867 * @todo Remove this hack once the base64 error is removed from the Theme Check.
16868 *
16869 * @author Vova Feldman (@svovaf)
16870 * @since 1.2.2
16871 */
16872 $fn = 'base64' . '_encode';
16873
16874 return $fn( $str );
16875 }
16876
16877 static function _decrypt( $str ) {
16878 if ( is_null( $str ) ) {
16879 return null;
16880 }
16881
16882 /**
16883 * The encrypt/decrypt functions are used to protect
16884 * the user from messing up with some of the sensitive
16885 * data stored for the module as a JSON in the database.
16886 *
16887 * I used the same suggested hack by the theme review team.
16888 * For more details, look at the function `Base64UrlDecode()`
16889 * in `./sdk/FreemiusBase.php`.
16890 *
16891 * @todo Remove this hack once the base64 error is removed from the Theme Check.
16892 *
16893 * @author Vova Feldman (@svovaf)
16894 * @since 1.2.2
16895 */
16896 $fn = 'base64' . '_decode';
16897
16898 return $fn( $str );
16899 }
16900
16901 /**
16902 * @author Vova Feldman (@svovaf)
16903 * @since 1.0.5
16904 *
16905 * @param FS_Entity $entity
16906 *
16907 * @return FS_Entity Return an encrypted clone entity.
16908 */
16909 private static function _encrypt_entity( FS_Entity $entity ) {
16910 $clone = clone $entity;
16911 $props = get_object_vars( $entity );
16912
16913 foreach ( $props as $key => $val ) {
16914 $clone->{$key} = self::_encrypt( $val );
16915 }
16916
16917 return $clone;
16918 }
16919
16920 /**
16921 * @author Vova Feldman (@svovaf)
16922 * @since 1.0.5
16923 *
16924 * @param FS_Entity $entity
16925 *
16926 * @return FS_Entity Return an decrypted clone entity.
16927 */
16928 private static function decrypt_entity( FS_Entity $entity ) {
16929 $clone = clone $entity;
16930 $props = get_object_vars( $entity );
16931
16932 foreach ( $props as $key => $val ) {
16933 $clone->{$key} = self::_decrypt( $val );
16934 }
16935
16936 return $clone;
16937 }
16938
16939 /**
16940 * Tries to activate account based on POST params.
16941 *
16942 * @author Vova Feldman (@svovaf)
16943 * @since 1.0.2
16944 *
16945 * @deprecated Not in use, outdated.
16946 */
16947 function _activate_account() {
16948 if ( $this->is_registered() ) {
16949 // Already activated.
16950 return;
16951 }
16952
16953 self::_clean_admin_content_section();
16954
16955 if ( fs_request_is_action( 'activate' ) && fs_request_is_post() ) {
16956 // check_admin_referer( 'activate_' . $this->_plugin->public_key );
16957
16958 // Verify matching plugin details.
16959 if ( $this->_plugin->id != fs_request_get( 'plugin_id' ) || $this->_slug != fs_request_get( 'plugin_slug' ) ) {
16960 return;
16961 }
16962
16963 $user = new FS_User();
16964 $user->id = fs_request_get( 'user_id' );
16965 $user->public_key = fs_request_get( 'user_public_key' );
16966 $user->secret_key = fs_request_get( 'user_secret_key' );
16967 $user->email = fs_request_get( 'user_email' );
16968 $user->first = fs_request_get( 'user_first' );
16969 $user->last = fs_request_get( 'user_last' );
16970 $user->is_verified = fs_request_get_bool( 'user_is_verified' );
16971
16972 $site = new FS_Site();
16973 $site->id = fs_request_get( 'install_id' );
16974 $site->public_key = fs_request_get( 'install_public_key' );
16975 $site->secret_key = fs_request_get( 'install_secret_key' );
16976 $site->plan_id = fs_request_get( 'plan_id' );
16977
16978 $plans = array();
16979 $plans_data = json_decode( urldecode( fs_request_get( 'plans' ) ) );
16980 foreach ( $plans_data as $p ) {
16981 $plan = new FS_Plugin_Plan( $p );
16982 if ( $site->plan_id == $plan->id ) {
16983 $plan->title = fs_request_get( 'plan_title' );
16984 $plan->name = fs_request_get( 'plan_name' );
16985 }
16986
16987 $plans[] = $plan;
16988 }
16989
16990 $this->_set_account( $user, $site, $plans );
16991
16992 // Reload the page with the keys.
16993 fs_redirect( $this->_get_admin_page_url() );
16994 }
16995 }
16996
16997 /**
16998 * @author Vova Feldman (@svovaf)
16999 * @since 1.0.7
17000 *
17001 * @param string $email
17002 *
17003 * @return FS_User|false
17004 */
17005 static function _get_user_by_email( $email ) {
17006 self::$_static_logger->entrance();
17007
17008 $email = trim( strtolower( $email ) );
17009
17010 $users = self::get_all_users();
17011
17012 if ( is_array( $users ) ) {
17013 foreach ( $users as $user ) {
17014 if ( $email === trim( strtolower( $user->email ) ) ) {
17015 return $user;
17016 }
17017 }
17018 }
17019
17020 return false;
17021 }
17022
17023 #----------------------------------------------------------------------------------
17024 #region Account (Loading, Updates & Activation)
17025 #----------------------------------------------------------------------------------
17026
17027 /***
17028 * Load account information (user + site).
17029 *
17030 * @author Vova Feldman (@svovaf)
17031 * @since 1.0.1
17032 */
17033 private function _load_account() {
17034 $this->_logger->entrance();
17035
17036 $this->do_action( 'before_account_load' );
17037
17038 $users = self::get_all_users();
17039 $plans = self::get_all_plans( $this->_module_type );
17040
17041 if ( $this->_logger->is_on() && is_admin() ) {
17042 $this->_logger->log( 'users = ' . var_export( $users, true ) );
17043 $this->_logger->log( 'plans = ' . var_export( $plans, true ) );
17044 }
17045
17046 $site = fs_is_network_admin() ?
17047 $this->get_network_install() :
17048 $this->get_install_by_blog_id();
17049
17050 if ( fs_is_network_admin() &&
17051 $this->is_network_active() &&
17052 ! is_object( $site ) &&
17053 FS_Site::is_valid_id( $this->_storage->network_install_blog_id )
17054 ) {
17055 $first_install = $this->find_first_install();
17056
17057 if ( is_null( $first_install ) ) {
17058 unset( $this->_storage->network_install_blog_id );
17059 } else {
17060 $site = $first_install['install'];
17061 $this->_storage->network_install_blog_id = $first_install['blog_id'];
17062 }
17063 }
17064
17065 if ( is_object( $site ) &&
17066 is_numeric( $site->id ) &&
17067 is_numeric( $site->user_id ) &&
17068 FS_Plugin_Plan::is_valid_id( $site->plan_id )
17069 ) {
17070 // Load site.
17071 $this->_site = $site;
17072 }
17073
17074 $user = null;
17075 if ( fs_is_network_admin() && $this->_is_network_active ) {
17076 $user = $this->get_network_user();
17077 }
17078
17079 if ( is_object( $user ) ) {
17080 $this->_user = clone $user;
17081 } else if ( $this->_site ) {
17082 $user = self::_get_user_by_id( $this->_site->user_id );
17083
17084 if ( ! is_object( $user ) && FS_User::is_valid_id( $this->_storage->prev_user_id ) ) {
17085 /**
17086 * Try to load the previous owner. This recovery is used for the following use-case:
17087 * 1. Opt-in
17088 * 2. Cloning site1 to site2
17089 * 3. Ownership switch in site1 (same applies for site2)
17090 * 4. Install data sync on site2
17091 * 5. Now site2's install is associated with the new owner which does not exists locally.
17092 */
17093 $user = self::_get_user_by_id( $this->_storage->prev_user_id );
17094 }
17095
17096 if ( ! is_object( $user ) ) {
17097 /**
17098 * This is a special fault tolerance mechanism to handle a scenario that the user data is missing.
17099 */
17100 if (
17101 ! isset( $this->_storage->user_recovery_from_install_last_attempt_timestamp ) ||
17102 time() > ( $this->_storage->user_recovery_from_install_last_attempt_timestamp + FS_Clone_Manager::CLONE_RESOLUTION_MAX_EXECUTION_TIME )
17103 ) {
17104 $user = $this->sync_user_by_current_install();
17105 } else {
17106 return;
17107 }
17108
17109 if ( is_object( $user ) ) {
17110 $this->_storage->user_was_recovered_from_install = true;
17111 } else {
17112 $this->_storage->user_recovery_from_install_attempts = isset( $this->_storage->user_recovery_from_install_attempts ) ?
17113 ( $this->_storage->user_recovery_from_install_attempts + 1 ) :
17114 1;
17115
17116 if ( $this->_storage->user_recovery_from_install_attempts >= 3 ) {
17117 $this->delete_current_install( false );
17118 } else {
17119 $this->_storage->user_recovery_from_install_last_attempt_timestamp = time();
17120
17121 return;
17122 }
17123 }
17124 }
17125
17126 $this->_user = ( $user instanceof FS_User ) ?
17127 clone $user :
17128 null;
17129 }
17130
17131 if ( is_object( $this->_user ) ) {
17132 // Load licenses.
17133 $this->_licenses = $this->get_user_licenses( $this->_user->id );
17134 }
17135
17136 if ( is_object( $this->_site ) ) {
17137 // Load plans.
17138 $this->_plans = isset( $plans[ $this->_slug ] ) ?
17139 $plans[ $this->_slug ] :
17140 array();
17141
17142 if ( ! is_array( $this->_plans ) || empty( $this->_plans ) ) {
17143 $this->_sync_plans();
17144 } else {
17145 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
17146 if ( $this->_plans[ $i ] instanceof FS_Plugin_Plan ) {
17147 $this->_plans[ $i ] = self::decrypt_entity( $this->_plans[ $i ] );
17148 } else {
17149 unset( $this->_plans[ $i ] );
17150 }
17151 }
17152 }
17153
17154 $this->_license = $this->_get_license_by_id( $this->_site->license_id );
17155
17156 if ( $this->_site->version != $this->get_plugin_version() ) {
17157 // If stored install version is different than current installed plugin version,
17158 // then update plugin version event.
17159 $this->update_plugin_version_event();
17160 }
17161 }
17162
17163 if ( true === $this->_storage->require_license_activation &&
17164 ! fs_request_get_bool( 'require_license', true )
17165 ) {
17166 $this->_storage->require_license_activation = false;
17167 }
17168
17169 if ( $this->is_theme() ) {
17170 $this->_register_account_hooks();
17171 }
17172
17173 if ( $this->is_user_in_admin() && $this->is_clone() ) {
17174 if ( empty( FS_Clone_Manager::instance()->get_clone_identification_timestamp() ) ) {
17175 FS_Clone_Manager::instance()->store_clone_identification_timestamp();
17176 }
17177
17178 FS_Clone_Manager::instance()->maybe_update_clone_resolution_support_flag( $this->_storage->sdk_last_version );
17179 $this->send_pending_clone_update_once();
17180 }
17181 }
17182
17183 /**
17184 * Special user recovery mechanism.
17185 *
17186 * @author Vova Feldman (@svovaf)
17187 * @since 2.0.0
17188 *
17189 * @param number|null $site_user_id
17190 *
17191 * @return \FS_User|mixed
17192 */
17193 private function sync_user_by_current_install( $site_user_id = null ) {
17194 $site_user_id = FS_Site::is_valid_id( $site_user_id ) ?
17195 $site_user_id :
17196 $this->_site->user_id;
17197
17198 $api = $this->get_api_site_scope();
17199
17200 $uid = $this->get_anonymous_id();
17201 $request_path = "/users/{$site_user_id}.json?uid={$uid}";
17202
17203 $result = $api->get( $request_path, false, WP_FS__TIME_10_MIN_IN_SEC );
17204
17205 if ( $this->is_api_result_entity( $result ) ) {
17206 $user = new FS_User( $result );
17207 $this->_user = $user;
17208 $this->_store_user();
17209
17210 return $user;
17211 }
17212
17213 $error_code = FS_Api::get_error_code( $result );
17214
17215 if ( in_array( $error_code, array( 'invalid_unique_id', 'user_cannot_be_recovered' ) ) ) {
17216 /**
17217 * Those API errors will continue coming and are not recoverable with the
17218 * current site's data. Therefore, extend the API call's cached result to 7 days.
17219 */
17220 $api->update_cache_expiration( $request_path, WP_FS__TIME_WEEK_IN_SEC );
17221 }
17222
17223 return $result;
17224 }
17225
17226 /**
17227 * @author Vova Feldman (@svovaf)
17228 * @since 1.0.1
17229 *
17230 * @param FS_User $user
17231 * @param FS_Site $site
17232 * @param bool|array $plans
17233 */
17234 private function _set_account( FS_User $user, FS_Site $site, $plans = false ) {
17235 $site->user_id = $user->id;
17236
17237 $this->_site = $site;
17238 $this->_user = $user;
17239 if ( false !== $plans ) {
17240 $this->_plans = $plans;
17241 }
17242
17243 $this->send_install_update();
17244
17245 $this->_store_account();
17246
17247 }
17248
17249 /**
17250 * Get a sanitized array with the WordPress version, SDK version, and PHP version.
17251 * Each version is trimmed after the 16th char.
17252 *
17253 * @author Vova Feldman (@svovaf)
17254 * @since 2.2.1
17255 *
17256 * @return array
17257 */
17258 private function get_versions() {
17259 $versions = array();
17260 $versions['sdk_version'] = $this->version;
17261
17262 // Collect these diagnostic information only if it's allowed.
17263 if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) {
17264 $versions['platform_version'] = get_bloginfo( 'version' );
17265 $versions['programming_language_version'] = phpversion();
17266 }
17267
17268 foreach ( $versions as $k => $version ) {
17269 $versions[ $k ] = self::get_api_sanitized_property( $k, $version );
17270 }
17271
17272 return $versions;
17273 }
17274
17275 /**
17276 * Get sanitized site language.
17277 *
17278 * @param string $language
17279 * @param int $max_len
17280 *
17281 * @since 2.5.1
17282 * @author Vova Feldman (@svovaf)
17283 *
17284 * @return string
17285 */
17286 private static function get_sanitized_language( $language = '', $max_len = self::LANGUAGE_MAX_CHARS ) {
17287 if ( empty( $language ) ) {
17288 $language = get_bloginfo( 'language' );
17289 }
17290
17291 return substr( $language, 0, $max_len );
17292 }
17293
17294 /**
17295 * Get core version stripped from pre-release and build.
17296 *
17297 * @since 2.5.1
17298 * @author Vova Feldman (@svovaf)
17299 *
17300 * @param string $version
17301 * @param int $parts
17302 * @param int $max_len
17303 * @param bool $include_pre_release
17304 *
17305 * @return string
17306 */
17307 private static function get_core_version(
17308 $version,
17309 $parts = 3,
17310 $max_len = self::VERSION_MAX_CHARS,
17311 $include_pre_release = false
17312 ) {
17313 if ( empty( $version ) ) {
17314 // Version is empty.
17315 return '';
17316 }
17317
17318 if ( is_numeric( $version ) ) {
17319 $is_float_version = is_float( $version );
17320
17321 $version = (string) $version;
17322
17323 /**
17324 * 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.
17325 */
17326 if ( $is_float_version && false === strpos( $version, '.' ) ) {
17327 $version .= '.0';
17328 }
17329 }
17330
17331 if ( ! is_string( $version ) ) {
17332 return '';
17333 }
17334
17335 if ( $parts < 1 ) {
17336 return '';
17337 }
17338
17339 $pre_release_regex = $include_pre_release ?
17340 '(\-(alpha|beta|RC)([0-9]+)?)?' :
17341 '';
17342
17343 if ( 0 === preg_match( '/^([0-9]+(\.[0-9]+){0,' . ( $parts - 1 ) . '}' . $pre_release_regex . ')/i', $version, $matches ) ) {
17344 // Version is not starting with a digit.
17345 return '';
17346 }
17347
17348 return substr( $matches[1], 0, $max_len );
17349 }
17350
17351 /**
17352 * @param string $prop
17353 * @param mixed $val
17354 *
17355 * @return mixed
17356 *@author Vova Feldman (@svovaf)
17357 *
17358 * @since 2.5.1
17359 */
17360 private static function get_api_sanitized_property( $prop, $val ) {
17361 if ( ! is_string( $val ) || empty( $val ) ) {
17362 return $val;
17363 }
17364
17365 switch ( $prop ) {
17366 case 'programming_language_version':
17367 // Get core PHP version, which can have up to 3 parts (ignore pre-releases).
17368 return self::get_core_version( $val );
17369 case 'platform_version':
17370 // Get the exact WordPress version, which can have up to 3 parts (including pre-releases).
17371 return self::get_core_version( $val, 3, self::VERSION_MAX_CHARS, true );
17372 case 'sdk_version':
17373 // Get the exact SDK version, which can have up to 4 parts.
17374 return self::get_core_version( $val, 4 );
17375 case 'version':
17376 // Get the entire version but just limited in length.
17377 return substr( $val, 0, self::VERSION_MAX_CHARS );
17378 case 'language':
17379 return self::get_sanitized_language( $val );
17380 default:
17381 return $val;
17382 }
17383 }
17384
17385 /**
17386 * @author Leo Fajardo (@leorw)
17387 * @since 2.3.0
17388 *
17389 * @return bool
17390 */
17391 function has_beta_update() {
17392 return (
17393 ! empty( $this->_storage->beta_data ) &&
17394 ( true === $this->_storage->beta_data['is_beta'] ) &&
17395 version_compare( $this->_storage->beta_data['version'], $this->get_plugin_version(), '>' )
17396 );
17397 }
17398
17399 /**
17400 * @author Leo Fajardo (@leorw)
17401 * @since 2.3.0
17402 *
17403 * @return bool
17404 */
17405 function is_beta() {
17406 return (
17407 ! empty( $this->_storage->beta_data ) &&
17408 ( true === $this->_storage->beta_data['is_beta'] ) &&
17409 ( $this->get_plugin_version() === $this->_storage->beta_data['version'] )
17410 );
17411 }
17412
17413 /**
17414 * @author Vova Feldman (@svovaf)
17415 * @since 1.1.7.4
17416 *
17417 * @param array $override_with
17418 * @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.
17419 *
17420 * @return array
17421 */
17422 function get_opt_in_params( $override_with = array(), $network_level_or_blog_id = null ) {
17423 $this->_logger->entrance();
17424
17425 $current_user = self::_get_current_wp_user();
17426
17427 $activation_action = $this->get_unique_affix() . '_activate_new';
17428 $return_url = $this->is_anonymous() ?
17429 // If skipped already, then return to the account page.
17430 $this->get_account_url( $activation_action, array(), false ) :
17431 // Return to the module's main page.
17432 $this->get_after_activation_url( 'after_connect_url', array( 'fs_action' => $activation_action ) );
17433
17434 $versions = $this->get_versions();
17435
17436 $params = array_merge( $versions, array(
17437 'user_firstname' => $current_user->user_firstname,
17438 'user_lastname' => $current_user->user_lastname,
17439 'user_email' => $current_user->user_email,
17440 'plugin_slug' => $this->_slug,
17441 'plugin_id' => $this->get_id(),
17442 'plugin_public_key' => $this->get_public_key(),
17443 'plugin_version' => $this->get_plugin_version(),
17444 'return_url' => fs_nonce_url( $return_url, $activation_action ),
17445 'account_url' => fs_nonce_url( $this->_get_admin_page_url(
17446 'account',
17447 array( 'fs_action' => 'sync_user' )
17448 ), 'sync_user' ),
17449 'is_premium' => $this->is_premium(),
17450 'is_active' => true,
17451 'is_uninstalled' => false,
17452 'is_localhost' => WP_FS__IS_LOCALHOST,
17453 ) );
17454
17455 if ( $this->is_addon() ) {
17456 $parent_fs = $this->get_parent_instance();
17457
17458 $params['parent_plugin_slug'] = $parent_fs->_slug;
17459 $params['parent_plugin_id'] = $parent_fs->get_id();
17460 }
17461
17462 if ( true === $network_level_or_blog_id ) {
17463 if ( ! isset( $override_with['sites'] ) ) {
17464 $params['sites'] = $this->get_sites_for_network_level_optin();
17465 }
17466 } else {
17467 $site = is_numeric( $network_level_or_blog_id ) ?
17468 array( 'blog_id' => $network_level_or_blog_id ) :
17469 null;
17470
17471 $site = $this->get_site_info( $site );
17472
17473 $diagnostic_info = array();
17474 if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) {
17475 $diagnostic_info = array(
17476 'site_name' => $site['title'],
17477 'language' => self::get_sanitized_language( $site['language'] ),
17478 );
17479 }
17480
17481 $params = array_merge( $params, $diagnostic_info, array(
17482 'site_uid' => $site['uid'],
17483 'site_url' => $site['url'],
17484 ) );
17485 }
17486
17487 if ( $this->is_pending_activation() &&
17488 ! empty( $this->_storage->pending_license_key )
17489 ) {
17490 $params['license_key'] = $this->_storage->pending_license_key;
17491 }
17492
17493 if ( WP_FS__SKIP_EMAIL_ACTIVATION && $this->has_secret_key() ) {
17494 // Even though rand() is known for its security issues,
17495 // the timestamp adds another layer of protection.
17496 // It would be very hard for an attacker to get the secret key form here.
17497 // Plus, this should never run in production since the secret should never
17498 // be included in the production version.
17499 $params['ts'] = WP_FS__SCRIPT_START_TIME;
17500 $params['salt'] = md5( uniqid( rand() ) );
17501 $params['secure'] = md5(
17502 $params['ts'] .
17503 $params['salt'] .
17504 $this->get_secret_key()
17505 );
17506 }
17507
17508 if ( is_multisite() && function_exists( 'get_network' ) ) {
17509 $params['network_uid'] = $this->get_anonymous_network_id();
17510 }
17511
17512 return array_merge( $params, $override_with );
17513 }
17514
17515 /**
17516 * 1. If successful opt-in or pending activation returns the next page that the user should be redirected to.
17517 * 2. If there was an API error, return the API result.
17518 *
17519 * @author Vova Feldman (@svovaf)
17520 * @since 1.1.7.4
17521 *
17522 * @param string|bool $email
17523 * @param string|bool $first
17524 * @param string|bool $last
17525 * @param string|bool $license_key
17526 * @param bool $is_uninstall If "true", this means that the module is currently being uninstalled.
17527 * In this case, the user and site info will be sent to the server but no
17528 * data will be saved to the WP installation's database.
17529 * @param number|bool $trial_plan_id
17530 * @param bool $is_disconnected Whether to opt in without tracking.
17531 * @param null|bool $is_marketing_allowed
17532 * @param array $sites If network-level opt-in, an array of containing details of sites.
17533 * @param bool $redirect
17534 *
17535 * @return string|object
17536 * @use WP_Error
17537 */
17538 function opt_in(
17539 $email = false,
17540 $first = false,
17541 $last = false,
17542 $license_key = false,
17543 $is_uninstall = false,
17544 $trial_plan_id = false,
17545 $is_disconnected = false,
17546 $is_marketing_allowed = null,
17547 $sites = array(),
17548 $redirect = true
17549 ) {
17550 $this->_logger->entrance();
17551
17552 if ( false === $email ) {
17553 $current_user = self::_get_current_wp_user();
17554 $email = $current_user->user_email;
17555 }
17556
17557 /**
17558 * @since 1.2.1 If activating with license key, ignore the context-user
17559 * since the user will be automatically loaded from the license.
17560 */
17561 if ( empty( $license_key ) ) {
17562 // Clean up pending license if opt-ing in again.
17563 $this->_storage->remove( 'pending_license_key' );
17564
17565 if ( ! $is_uninstall ) {
17566 $fs_user = Freemius::_get_user_by_email( $email );
17567 if ( is_object( $fs_user ) && ! $this->is_pending_activation() ) {
17568 return $this->install_with_user(
17569 $fs_user,
17570 false,
17571 $trial_plan_id,
17572 $redirect,
17573 true,
17574 $sites
17575 );
17576 }
17577 }
17578 }
17579
17580 $user_info = array();
17581 if ( ! empty( $email ) ) {
17582 $user_info['user_email'] = $email;
17583 }
17584 if ( ! empty( $first ) ) {
17585 $user_info['user_firstname'] = $first;
17586 }
17587 if ( ! empty( $last ) ) {
17588 $user_info['user_lastname'] = $last;
17589 }
17590
17591 if ( ! empty( $sites ) ) {
17592 $is_network = true;
17593
17594 $user_info['sites'] = $sites;
17595 } else {
17596 $is_network = false;
17597 }
17598
17599 $params = $this->get_opt_in_params( $user_info, $is_network );
17600
17601 $filtered_license_key = false;
17602 if ( is_string( $license_key ) ) {
17603 $filtered_license_key = $this->apply_filters( 'license_key', $license_key );
17604 $params['license_key'] = $filtered_license_key;
17605 } else if ( FS_Plugin_Plan::is_valid_id( $trial_plan_id ) ) {
17606 $params['trial_plan_id'] = $trial_plan_id;
17607 }
17608
17609 if ( $is_uninstall ) {
17610 $params['uninstall_params'] = array(
17611 'reason_id' => $this->_storage->uninstall_reason->id,
17612 'reason_info' => $this->_storage->uninstall_reason->info
17613 );
17614 }
17615
17616 if ( isset( $params['license_key'] ) ) {
17617 $fs_user = Freemius::_get_user_by_email( $email );
17618
17619 if ( is_object( $fs_user ) ) {
17620 /**
17621 * If opting in with a context license and the context WP Admin user already opted in
17622 * before from the current site, add the user context security params to avoid the
17623 * unnecessary email activation when the context license is owned by the same context user.
17624 *
17625 * @author Leo Fajardo (@leorw)
17626 * @since 1.2.3
17627 */
17628 $params = array_merge( $params, FS_Security::instance()->get_context_params(
17629 $fs_user,
17630 false,
17631 'install_with_existing_user'
17632 ) );
17633 }
17634 }
17635
17636 if ( is_bool( $is_marketing_allowed ) ) {
17637 $params['is_marketing_allowed'] = $is_marketing_allowed;
17638 }
17639
17640 $params['is_disconnected'] = $is_disconnected;
17641 $params['format'] = 'json';
17642 $params['is_extensions_tracking_allowed'] = FS_Permission_Manager::instance( $this )->is_extensions_tracking_allowed();
17643 $params['is_diagnostic_tracking_allowed'] = FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed();
17644
17645 $request = array(
17646 'method' => 'POST',
17647 'body' => $params,
17648 'timeout' => 60,
17649 );
17650
17651 $url = $this->add_show_pending( WP_FS__ADDRESS . '/action/service/user/install/' );
17652 $response = self::safe_remote_post( $url, $request );
17653
17654 if ( is_wp_error( $response ) ) {
17655 /**
17656 * @var WP_Error $response
17657 */
17658 $result = new stdClass();
17659
17660 $error_code = $response->get_error_code();
17661 $error_type = str_replace( ' ', '', ucwords( str_replace( '_', ' ', $error_code ) ) );
17662
17663 $result->error = (object) array(
17664 'type' => $error_type,
17665 'message' => $response->get_error_message(),
17666 'code' => $error_code,
17667 'http' => 402
17668 );
17669
17670 $this->maybe_modify_api_curl_error_message( $result );
17671
17672 return $result;
17673 }
17674
17675 // Module is being uninstalled, don't handle the returned data.
17676 if ( $is_uninstall ) {
17677 return true;
17678 }
17679
17680 /**
17681 * 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.
17682 *
17683 * @author Vova Feldman (@svovaf)
17684 * @since 1.2.3
17685 * @link https://themes.trac.wordpress.org/ticket/46134#comment:5
17686 * @link https://themes.trac.wordpress.org/ticket/46134#comment:9
17687 * @link https://themes.trac.wordpress.org/ticket/46134#comment:12
17688 * @link https://themes.trac.wordpress.org/ticket/46134#comment:14
17689 */
17690 $decoded = is_string( $response['body'] ) ?
17691 json_decode( $response['body'] ) :
17692 null;
17693
17694 if ( empty( $decoded ) ) {
17695 return false;
17696 }
17697
17698 if ( ! $this->is_api_result_object( $decoded ) ) {
17699 if ( ! empty( $params['license_key'] ) ) {
17700 // Pass the fully entered license key to the failure handler.
17701 $params['license_key'] = $license_key;
17702 }
17703
17704 return $is_uninstall ?
17705 $decoded :
17706 $this->apply_filters( 'after_install_failure', $decoded, $params );
17707 } else if ( isset( $decoded->pending_activation ) && $decoded->pending_activation ) {
17708 if ( $is_network ) {
17709 $site_ids = array();
17710 foreach ( $sites as $site ) {
17711 $site_ids[] = $site['blog_id'];
17712 }
17713
17714 /**
17715 * Store the sites so that they can be installed once the user has clicked on the activation link
17716 * in the email.
17717 *
17718 * @author Leo Fajardo (@leorw)
17719 */
17720 $this->_storage->pending_sites_info = array(
17721 'blog_ids' => $site_ids,
17722 'license_key' => $license_key,
17723 'trial_plan_id' => $trial_plan_id
17724 );
17725 }
17726
17727 // Pending activation, add message.
17728 return $this->set_pending_confirmation(
17729 ( isset( $decoded->email ) ?
17730 $decoded->email :
17731 true ),
17732 false,
17733 $filtered_license_key,
17734 ! empty( $params['trial_plan_id'] ),
17735 isset( $decoded->is_suspicious_email ) && $decoded->is_suspicious_email
17736 );
17737 } else if ( isset( $decoded->install_secret_key ) ) {
17738 return $this->install_with_new_user(
17739 $decoded->user_id,
17740 $decoded->user_public_key,
17741 $decoded->user_secret_key,
17742 ( isset( $decoded->is_marketing_allowed ) && ! is_null( $decoded->is_marketing_allowed ) ?
17743 $decoded->is_marketing_allowed :
17744 null ),
17745 ( isset( $decoded->is_extensions_tracking_allowed ) && ! is_null( $decoded->is_extensions_tracking_allowed ) ?
17746 $decoded->is_extensions_tracking_allowed :
17747 null ),
17748 ( isset( $decoded->is_diagnostic_tracking_allowed ) && ! is_null( $decoded->is_diagnostic_tracking_allowed ) ?
17749 $decoded->is_diagnostic_tracking_allowed :
17750 null ),
17751 $decoded->install_id,
17752 $decoded->install_public_key,
17753 $decoded->install_secret_key,
17754 false
17755 );
17756 } else if ( is_array( $decoded->installs ) ) {
17757 return $this->install_many_with_new_user(
17758 $decoded->user_id,
17759 $decoded->user_public_key,
17760 $decoded->user_secret_key,
17761 ( isset( $decoded->is_marketing_allowed ) && ! is_null( $decoded->is_marketing_allowed ) ?
17762 $decoded->is_marketing_allowed :
17763 null ),
17764 ( isset( $decoded->is_extensions_tracking_allowed ) && ! is_null( $decoded->is_extensions_tracking_allowed ) ?
17765 $decoded->is_extensions_tracking_allowed :
17766 null ),
17767 ( isset( $decoded->is_diagnostic_tracking_allowed ) && ! is_null( $decoded->is_diagnostic_tracking_allowed ) ?
17768 $decoded->is_diagnostic_tracking_allowed :
17769 null ),
17770 $decoded->installs,
17771 false
17772 );
17773 }
17774
17775 return $decoded;
17776 }
17777
17778 /**
17779 * Set user and site identities.
17780 *
17781 * @author Vova Feldman (@svovaf)
17782 * @since 1.0.9
17783 *
17784 * @param FS_User $user
17785 * @param FS_Site $site
17786 * @param bool $redirect
17787 * @param bool $auto_install Since 1.2.1.7 If `true` and setting up an account with a valid license, will
17788 * redirect (or return a URL) to the account page with a special parameter to
17789 * trigger the auto installation processes.
17790 *
17791 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17792 */
17793 function setup_account(
17794 FS_User $user,
17795 FS_Site $site,
17796 $redirect = true,
17797 $auto_install = false
17798 ) {
17799 return $this->setup_network_account(
17800 $user,
17801 array( $site ),
17802 $redirect,
17803 $auto_install,
17804 false
17805 );
17806 }
17807
17808 /**
17809 * Set user and site identities.
17810 *
17811 * @author Vova Feldman (@svovaf)
17812 * @since 2.0.0
17813 *
17814 * @param FS_User $user
17815 * @param FS_Site[] $installs
17816 * @param bool $redirect
17817 * @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.
17818 * @param bool $is_network_level_opt_in
17819 *
17820 * @return string If redirect is `false`, returns the next page the user should be redirected to.
17821 */
17822 function setup_network_account(
17823 FS_User $user,
17824 array $installs,
17825 $redirect = true,
17826 $auto_install = false,
17827 $is_network_level_opt_in = true
17828 ) {
17829 $first_install = $installs[0];
17830
17831 $this->_user = $user;
17832 $this->_site = $first_install;
17833
17834 $this->_sync_plans();
17835
17836 if ( $this->_storage->handle_gdpr_admin_notice &&
17837 $this->should_handle_gdpr_admin_notice() &&
17838 FS_GDPR_Manager::instance()->should_show_opt_in_notice()
17839 ) {
17840 /**
17841 * Clear user lock after an opt-in.
17842 */
17843 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
17844 FS_User_Lock::instance()->unlock();
17845 }
17846
17847 if ( 1 < count( $installs ) ) {
17848 // Only network level opt-in can have more than one install.
17849 $is_network_level_opt_in = true;
17850 }
17851 // $is_network_level_opt_in = self::is_ajax_action_static( 'network_activate', $this->_module_id );
17852 // If Freemius was OFF before, turn it on.
17853 $this->turn_on();
17854
17855 $this->handle_account_connection(
17856 $installs,
17857 ( ! $this->_is_network_active || ! $is_network_level_opt_in )
17858 );
17859
17860 if ( is_numeric( $first_install->license_id ) ) {
17861 $this->set_license( $this->_get_license_by_id( $first_install->license_id ) );
17862 }
17863
17864 $this->_admin_notices->remove_sticky( 'connect_account' );
17865
17866 if ( $this->is_pending_activation() || ! $this->has_settings_menu() ) {
17867 $this->clear_pending_activation_mode();
17868
17869 if ( ! $this->is_paying_or_trial() ) {
17870 $this->_admin_notices->add_sticky(
17871 sprintf( $this->get_text_inline( '%s opt-in was successfully completed.', 'plugin-x-activation-message' ), '<b>' . $this->get_plugin_name() . '</b>' ),
17872 'activation_complete'
17873 );
17874 }
17875 }
17876
17877 if ( $this->is_paying_or_trial() ) {
17878 if ( ! $this->is_premium() ||
17879 ! $this->has_premium_version() ||
17880 ! $this->has_settings_menu()
17881 ) {
17882 if ( $this->is_paying() ) {
17883 $this->_admin_notices->add_sticky(
17884 sprintf(
17885 $this->get_text_inline( 'Your account was successfully activated with the %s plan.', 'activation-with-plan-x-message' ),
17886 $this->get_plan_title()
17887 ) . $this->get_complete_upgrade_instructions(),
17888 'plan_upgraded',
17889 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
17890 );
17891 } else {
17892 $trial_plan = $this->get_trial_plan();
17893
17894 $this->_admin_notices->add_sticky(
17895 sprintf(
17896 $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ),
17897 '<i>' . $this->get_plugin_name() . '</i>'
17898 ) . $this->get_complete_upgrade_instructions( $trial_plan->title ),
17899 'trial_started',
17900 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
17901 );
17902 }
17903 }
17904
17905 $this->_admin_notices->remove_sticky( array(
17906 'trial_promotion',
17907 ) );
17908 }
17909
17910 $plugin_id = fs_request_get( 'plugin_id', false );
17911
17912 // Store activation time ONLY for plugins & themes (not add-ons).
17913 if ( ! is_numeric( $plugin_id ) || ( $plugin_id == $this->_plugin->id ) ) {
17914 if ( empty( $this->_storage->activation_timestamp ) ) {
17915 $this->_storage->activation_timestamp = WP_FS__SCRIPT_START_TIME;
17916 }
17917 }
17918
17919 $next_page = '';
17920
17921 $extra = array();
17922 if ( $auto_install ) {
17923 $extra['auto_install'] = 'true';
17924 }
17925
17926 if ( is_numeric( $plugin_id ) ) {
17927 /**
17928 * @author Leo Fajardo (@leorw)
17929 * @since 1.2.1.6
17930 *
17931 * Also sync the license after an anonymous user subscribes.
17932 */
17933 if ( $this->is_anonymous() || $plugin_id != $this->_plugin->id ) {
17934 // Add-on was installed - sync license right after install.
17935 $next_page = $this->_get_sync_license_url( $plugin_id, true, $extra );
17936 }
17937 } else {
17938 /**
17939 * @author Vova Feldman (@svovaf)
17940 * @since 1.1.9 If site installed with a valid license, sync license.
17941 */
17942 if ( $this->is_paying() ) {
17943 $this->_sync_plugin_license(
17944 true,
17945 // Installs data is already synced in the beginning of this method directly or via _set_account().
17946 false
17947 );
17948 }
17949
17950 // Reload the page with the keys.
17951 $next_page = $this->is_anonymous() ?
17952 // If user previously skipped, redirect to account page.
17953 $this->get_account_url( false, $extra ) :
17954 $this->get_after_activation_url( 'after_connect_url', array(), $is_network_level_opt_in );
17955 }
17956
17957 if ( ! empty( $next_page ) && $redirect ) {
17958 fs_redirect( $next_page );
17959 }
17960
17961 return $next_page;
17962 }
17963
17964 /**
17965 * Install plugin with new user information after approval.
17966 *
17967 * @author Vova Feldman (@svovaf)
17968 * @since 1.0.7
17969 */
17970 function _install_with_new_user() {
17971 $this->_logger->entrance();
17972
17973 if ( $this->is_registered() ) {
17974 return;
17975 }
17976
17977 if ( ( $this->is_plugin() && fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) ||
17978 // @todo This logic should be improved because it's executed on every load of a theme.
17979 $this->is_theme()
17980 ) {
17981 // check_admin_referer( $this->_slug . '_activate_new' );
17982
17983 if ( fs_request_has( 'user_secret_key' ) ) {
17984 if ( fs_is_network_admin() && isset( $this->_storage->pending_sites_info ) ) {
17985 $pending_sites_info = $this->_storage->pending_sites_info;
17986
17987 $this->install_many_pending_with_user(
17988 fs_request_get( 'user_id' ),
17989 fs_request_get( 'user_public_key' ),
17990 fs_request_get( 'user_secret_key' ),
17991 fs_request_get_bool( 'is_marketing_allowed', null ),
17992 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
17993 fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
17994 $pending_sites_info['blog_ids'],
17995 $pending_sites_info['license_key'],
17996 $pending_sites_info['trial_plan_id']
17997 );
17998 } else {
17999 $this->install_with_new_user(
18000 fs_request_get( 'user_id' ),
18001 fs_request_get( 'user_public_key' ),
18002 fs_request_get( 'user_secret_key' ),
18003 fs_request_get_bool( 'is_marketing_allowed', null ),
18004 fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
18005 fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
18006 fs_request_get( 'install_id' ),
18007 fs_request_get( 'install_public_key' ),
18008 fs_request_get( 'install_secret_key' ),
18009 true,
18010 fs_request_get_bool( 'auto_install' )
18011 );
18012 }
18013 } else if ( fs_request_has( 'pending_activation' ) ) {
18014 $this->set_pending_confirmation(
18015 fs_request_get( 'user_email' ),
18016 true,
18017 false,
18018 false,
18019 fs_request_get_bool( 'is_suspicious_email' )
18020 );
18021 }
18022 }
18023 }
18024
18025 /**
18026 * @author Vova Feldman (@svovaf)
18027 * @since 2.0.0
18028 *
18029 * @param number $id
18030 * @param string $public_key
18031 * @param string $secret_key
18032 *
18033 * @return \FS_User
18034 */
18035 private function setup_user( $id, $public_key, $secret_key ) {
18036 $user = self::_get_user_by_id( $id );
18037
18038 if ( is_object( $user ) ) {
18039 $this->_user = $user;
18040 } else {
18041 $user = new FS_User();
18042 $user->id = $id;
18043 $user->public_key = $public_key;
18044 $user->secret_key = $secret_key;
18045
18046 $this->_user = $user;
18047 $user_result = $this->get_api_user_scope()->get();
18048 $user = new FS_User( $user_result );
18049
18050 $this->_user = $user;
18051 $this->_store_user();
18052 }
18053
18054 return $user;
18055 }
18056
18057 /**
18058 * Install plugin with new user.
18059 *
18060 * @author Vova Feldman (@svovaf)
18061 * @since 1.1.7.4
18062 *
18063 * @param number $user_id
18064 * @param string $user_public_key
18065 * @param string $user_secret_key
18066 * @param bool|null $is_marketing_allowed
18067 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
18068 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2
18069 * @param number $install_id
18070 * @param string $install_public_key
18071 * @param string $install_secret_key
18072 * @param bool $redirect
18073 * @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.
18074 *
18075 * @return string If redirect is `false`, returns the next page the user should be redirected to.
18076 */
18077 private function install_with_new_user(
18078 $user_id,
18079 $user_public_key,
18080 $user_secret_key,
18081 $is_marketing_allowed,
18082 $is_extensions_tracking_allowed,
18083 $is_diagnostic_tracking_allowed,
18084 $install_id,
18085 $install_public_key,
18086 $install_secret_key,
18087 $redirect = true,
18088 $auto_install = false
18089 ) {
18090 /**
18091 * This method is also executed after opting in with a license key since the
18092 * license can be potentially associated with a different owner.
18093 *
18094 * @since 2.0.0
18095 */
18096 $user = self::_get_user_by_id( $user_id );
18097
18098 if ( ! is_object( $user ) ) {
18099 $user = new FS_User();
18100 $user->id = $user_id;
18101 $user->public_key = $user_public_key;
18102 $user->secret_key = $user_secret_key;
18103
18104 $this->_user = $user;
18105 $user_result = $this->get_api_user_scope()->get();
18106 $user = new FS_User( $user_result );
18107 }
18108
18109 $this->_user = $user;
18110
18111 $site = new FS_Site();
18112 $site->id = $install_id;
18113 $site->public_key = $install_public_key;
18114 $site->secret_key = $install_secret_key;
18115
18116 $this->_site = $site;
18117 $site_result = $this->get_api_site_scope( true )->get();
18118 $site = new FS_Site( $site_result );
18119 $this->_site = $site;
18120
18121 if ( ! is_null( $is_marketing_allowed ) ) {
18122 $this->disable_opt_in_notice_and_lock_user();
18123 }
18124
18125 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
18126 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
18127 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
18128 ) );
18129
18130 return $this->setup_account(
18131 $this->_user,
18132 $this->_site,
18133 $redirect,
18134 $auto_install
18135 );
18136 }
18137
18138 /**
18139 * Install plugin with user.
18140 *
18141 * @author Leo Fajardo (@leorw)
18142 * @since 2.0.0
18143 *
18144 * @param number $user_id
18145 * @param string $user_public_key
18146 * @param string $user_secret_key
18147 * @param bool|null $is_marketing_allowed
18148 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
18149 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2
18150 * @param array $site_ids
18151 * @param bool $license_key
18152 * @param bool $trial_plan_id
18153 * @param bool $redirect
18154 *
18155 * @return string If redirect is `false`, returns the next page the user should be redirected to.
18156 */
18157 private function install_many_pending_with_user(
18158 $user_id,
18159 $user_public_key,
18160 $user_secret_key,
18161 $is_marketing_allowed,
18162 $is_extensions_tracking_allowed,
18163 $is_diagnostic_tracking_allowed,
18164 $site_ids,
18165 $license_key = false,
18166 $trial_plan_id = false,
18167 $redirect = true
18168 ) {
18169 $user = $this->setup_user( $user_id, $user_public_key, $user_secret_key );
18170
18171 if ( ! is_null( $is_marketing_allowed ) ) {
18172 $this->disable_opt_in_notice_and_lock_user();
18173 }
18174
18175 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
18176 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
18177 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
18178 ) );
18179
18180 $sites = array();
18181 foreach ( $site_ids as $site_id ) {
18182 $sites[] = $this->get_site_info( array( 'blog_id' => $site_id ) );
18183 }
18184
18185 $this->install_with_user( $user, $license_key, $trial_plan_id, $redirect, true, $sites );
18186 }
18187
18188 /**
18189 * Multi-site install with a new user.
18190 *
18191 * @author Vova Feldman (@svovaf)
18192 * @since 2.0.0
18193 *
18194 * @param number $user_id
18195 * @param string $user_public_key
18196 * @param string $user_secret_key
18197 * @param bool|null $is_marketing_allowed
18198 * @param bool|null $is_extensions_tracking_allowed Since 2.3.2
18199 * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2
18200 * @param object[] $installs
18201 * @param bool $redirect
18202 * @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.
18203 *
18204 * @return string If redirect is `false`, returns the next page the user should be redirected to.
18205 */
18206 private function install_many_with_new_user(
18207 $user_id,
18208 $user_public_key,
18209 $user_secret_key,
18210 $is_marketing_allowed,
18211 $is_extensions_tracking_allowed,
18212 $is_diagnostic_tracking_allowed,
18213 array $installs,
18214 $redirect = true,
18215 $auto_install = false
18216 ) {
18217 $this->setup_user( $user_id, $user_public_key, $user_secret_key );
18218
18219 if ( ! is_null( $is_marketing_allowed ) ) {
18220 $this->disable_opt_in_notice_and_lock_user();
18221 }
18222
18223 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
18224 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed,
18225 FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed,
18226 ) );
18227
18228 $install_ids = array();
18229
18230 foreach ( $installs as $install ) {
18231 $install_ids[] = $install->id;
18232 }
18233
18234 $items_per_request = 25;
18235 $left = count( $install_ids );
18236 $offset = 0;
18237
18238 $installs = array();
18239 while ( $left > 0 ) {
18240 $result = $this->get_api_user_scope()->get( "/plugins/{$this->_module_id}/installs.json?ids=" . implode( ',', array_slice( $install_ids, $offset, $items_per_request ) ) );
18241
18242 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
18243 // @todo Handle API error.
18244 }
18245
18246 $installs = array_merge( $installs, $result->installs );
18247
18248 $left -= $items_per_request;
18249 $offset += $items_per_request;
18250 }
18251
18252 foreach ( $installs as &$install ) {
18253 $install = new FS_Site( $install );
18254 }
18255
18256 return $this->setup_network_account(
18257 $this->_user,
18258 $installs,
18259 $redirect,
18260 $auto_install
18261 );
18262 }
18263
18264 /**
18265 * @author Vova Feldman (@svovaf)
18266 * @since 1.1.7.4
18267 *
18268 * @param string|bool $email
18269 * @param bool $redirect
18270 * @param string|bool $license_key Since 1.2.1.5
18271 * @param bool $is_pending_trial Since 1.2.1.5
18272 *
18273 * @return string Since 1.2.1.5 if $redirect is `false`, return the pending activation page.
18274 */
18275 private function set_pending_confirmation(
18276 $email = false,
18277 $redirect = true,
18278 $license_key = false,
18279 $is_pending_trial = false,
18280 $is_suspicious_email = false
18281 ) {
18282 if ( $this->_ignore_pending_mode ) {
18283 /**
18284 * If explicitly asked to ignore pending mode, set to anonymous mode
18285 * if require confirmation before finalizing the opt-in.
18286 *
18287 * @author Vova Feldman
18288 * @since 1.2.1.6
18289 */
18290 $this->skip_connection( fs_is_network_admin() );
18291 } else {
18292 // Install must be activated via email since
18293 // user with the same email already exist.
18294 $this->_storage->is_pending_activation = true;
18295 $this->_add_pending_activation_notice( $email, $is_pending_trial, $is_suspicious_email );
18296 }
18297
18298 if ( ! empty( $license_key ) ) {
18299 $this->_storage->pending_license_key = $license_key;
18300 }
18301
18302 // Remove the opt-in sticky notice.
18303 $this->_admin_notices->remove_sticky( array(
18304 'connect_account',
18305 'trial_promotion',
18306 ) );
18307
18308 $next_page = $this->get_after_activation_url( 'after_pending_connect_url' );
18309
18310 if ( $redirect ) {
18311 // Reload the page with a pending activation message.
18312 fs_redirect( $next_page );
18313 }
18314
18315 return $next_page;
18316 }
18317
18318 /**
18319 * Install plugin with current logged WP user info.
18320 *
18321 * @author Vova Feldman (@svovaf)
18322 * @since 1.0.7
18323 */
18324 function _install_with_current_user() {
18325 $this->_logger->entrance();
18326
18327 if ( $this->is_registered() ) {
18328 return;
18329 }
18330
18331 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) && fs_request_is_post() ) {
18332 // check_admin_referer( 'activate_existing_' . $this->_plugin->public_key );
18333
18334 /**
18335 * @author Vova Feldman (@svovaf)
18336 * @since 1.1.9 Add license key if given.
18337 */
18338 $license_key = fs_request_get( 'license_secret_key' );
18339
18340 FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
18341 FS_Permission_Manager::PERMISSION_DIAGNOSTIC => fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
18342 FS_Permission_Manager::PERMISSION_EXTENSIONS => fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
18343 ) );
18344
18345 $this->install_with_current_user( $license_key );
18346 }
18347 }
18348
18349
18350 /**
18351 * @author Vova Feldman (@svovaf)
18352 * @since 1.1.7.4
18353 *
18354 * @param string|bool $license_key
18355 * @param number|bool $trial_plan_id
18356 * @param array $sites Since 2.0.0
18357 * @param bool $redirect
18358 *
18359 * @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.
18360 */
18361 function install_with_current_user(
18362 $license_key = false,
18363 $trial_plan_id = false,
18364 $sites = array(),
18365 $redirect = true
18366 ) {
18367 // Get current logged WP user.
18368 $current_user = self::_get_current_wp_user();
18369
18370 // Find the relevant FS user by the email.
18371 $user = self::_get_user_by_email( $current_user->user_email );
18372
18373 return $this->install_with_user( $user, $license_key, $trial_plan_id, $redirect, true, $sites );
18374 }
18375
18376 /**
18377 * @author Vova Feldman (@svovaf)
18378 * @since 2.0.0
18379 *
18380 * @param \FS_User $user
18381 * @param string|bool $license_key
18382 * @param number|bool $trial_plan_id
18383 * @param bool $redirect
18384 * @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.
18385 * @param array $sites Since 2.0.0. If not empty, should be a collection of site details for the bulk install API request.
18386 *
18387 * @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.
18388 */
18389 function install_with_user(
18390 FS_User $user,
18391 $license_key = false,
18392 $trial_plan_id = false,
18393 $redirect = true,
18394 $setup_account = true,
18395 $sites = array()
18396 ) {
18397 // We have to set the user before getting user scope API handler.
18398 $this->_user = $user;
18399
18400 // Install the plugin.
18401 $result = $this->create_installs_with_user(
18402 $user,
18403 $license_key,
18404 $trial_plan_id,
18405 $sites,
18406 $redirect
18407 );
18408
18409 if ( ! $this->is_api_result_entity( $result ) &&
18410 ! $this->is_api_result_object( $result, 'installs' )
18411 ) {
18412 // @todo Handler potential API error of the $result
18413 }
18414
18415 if ( empty( $sites ) ) {
18416 $site = new FS_Site( $result );
18417 $this->_site = $site;
18418
18419 if ( ! $setup_account ) {
18420 $this->_store_site();
18421
18422 $this->sync_plan_if_not_exist( $site->plan_id );
18423
18424 if ( ! empty( $license_key ) && FS_Plugin_License::is_valid_id( $site->license_id ) ) {
18425 $this->sync_license_if_not_exist( $site->license_id, $license_key );
18426 }
18427
18428 $this->_admin_notices->remove_sticky( 'connect_account', false );
18429
18430 return $site;
18431 }
18432
18433 return $this->setup_account( $this->_user, $this->_site, $redirect );
18434 } else {
18435 $installs = array();
18436 foreach ( $result->installs as $install ) {
18437 $installs[] = new FS_Site( $install );
18438 }
18439
18440 return $this->setup_network_account(
18441 $user,
18442 $installs,
18443 $redirect
18444 );
18445 }
18446 }
18447
18448 /**
18449 * Initiate an API request to create a collection of installs.
18450 *
18451 * @author Vova Feldman (@svovaf)
18452 * @since 2.0.0
18453 *
18454 * @param \FS_User $user
18455 * @param bool $license_key
18456 * @param bool $trial_plan_id
18457 * @param array $sites
18458 * @param bool $redirect
18459 * @param bool $silent
18460 *
18461 * @return object|mixed
18462 */
18463 private function create_installs_with_user(
18464 FS_User $user,
18465 $license_key = false,
18466 $trial_plan_id = false,
18467 $sites = array(),
18468 $redirect = false,
18469 $silent = false
18470 ) {
18471 $extra_install_params = array(
18472 'uid' => $this->get_anonymous_id(),
18473 'is_disconnected' => false,
18474 );
18475
18476 if ( ! empty( $license_key ) ) {
18477 $extra_install_params['license_key'] = $this->apply_filters( 'license_key', $license_key );
18478
18479 if ( $silent ) {
18480 $extra_install_params['ignore_license_owner'] = true;
18481 }
18482 } else if ( FS_Plugin_Plan::is_valid_id( $trial_plan_id ) ) {
18483 $extra_install_params['trial_plan_id'] = $trial_plan_id;
18484 }
18485
18486 if ( ! empty( $sites ) ) {
18487 $extra_install_params['sites'] = $sites;
18488 }
18489
18490 $args = $this->get_install_data_for_api( $extra_install_params, false, false );
18491
18492 // Install the plugin.
18493 $result = $this->get_api_user_scope_by_user( $user )->call(
18494 "/plugins/{$this->get_id()}/installs.json",
18495 'post',
18496 $args
18497 );
18498
18499 if ( ! $this->is_api_result_entity( $result ) &&
18500 ! $this->is_api_result_object( $result, 'installs' )
18501 ) {
18502 if ( ! empty( $args['license_key'] ) ) {
18503 // Pass the fully entered license key to the failure handler.
18504 $args['license_key'] = $license_key;
18505 }
18506
18507 $result = $this->apply_filters( 'after_install_failure', $result, $args );
18508
18509 if ( ! $silent ) {
18510 $this->_admin_notices->add(
18511 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
18512 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $result->error->message . '</b>',
18513 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
18514 'error'
18515 );
18516 }
18517
18518 if ( $redirect ) {
18519 /**
18520 * We set the user before getting the user scope API handler, so the user became temporarily
18521 * registered (`is_registered() = true`). Since the API returned an error and we will redirect,
18522 * we have to set the user to `null`, otherwise, the user will be redirected to the wrong
18523 * activation page based on the return value of `is_registered()`. In addition, in case the
18524 * context plugin doesn't have a settings menu and the default page is the `Plugins` page,
18525 * misleading plugin activation errors will be shown on the `Plugins` page.
18526 *
18527 * @author Leo Fajardo (@leorw)
18528 */
18529 $this->_user = null;
18530
18531 fs_redirect( $this->get_activation_url( array( 'error' => $result->error->message ) ) );
18532 }
18533 }
18534
18535 return $result;
18536 }
18537
18538 /**
18539 * Tries to activate add-on account based on parent plugin info.
18540 *
18541 * @author Vova Feldman (@svovaf)
18542 * @since 1.0.6
18543 *
18544 * @param Freemius $parent_fs
18545 * @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.
18546 * @param FS_Plugin_License $bundle_license Since 2.4.0. If provided, this license will be activated for the add-on.
18547 */
18548 private function _activate_addon_account(
18549 Freemius $parent_fs,
18550 $network_level_or_blog_id = null,
18551 FS_Plugin_License $bundle_license = null
18552 ) {
18553 if ( $this->is_registered() ) {
18554 // Already activated.
18555 return;
18556 }
18557
18558 /**
18559 * Do not override the `uid` if network-level opt-in since the call to `get_sites_for_network_level_optin()`
18560 * already returns the data for the current blog.
18561 *
18562 * @author Leo Fajardo (@leorw)
18563 * @since 2.3.0
18564 */
18565 $uid_param_to_override = ( true === $network_level_or_blog_id ) ?
18566 array() :
18567 array( 'uid' => $this->get_anonymous_id() );
18568
18569 $params = $this->get_install_data_for_api(
18570 $uid_param_to_override,
18571 false,
18572 false,
18573 /**
18574 * Do not include the data for the current blog if network-level opt-in since the call to `get_sites_for_network_level_optin`
18575 * already includes the data for it.
18576 *
18577 * @author Leo Fajardo (@leorw)
18578 * @since 2.3.0
18579 */
18580 ( true !== $network_level_or_blog_id )
18581 );
18582
18583 if ( true === $network_level_or_blog_id ) {
18584 $params['sites'] = $this->get_sites_for_network_level_optin();
18585
18586 if ( empty( $params['sites'] ) ) {
18587 return;
18588 }
18589 }
18590
18591 if ( is_object( $bundle_license ) ) {
18592 $params['license_key'] = $bundle_license->secret_key;
18593 }
18594
18595 // Activate add-on with parent plugin credentials.
18596 $result = $parent_fs->get_api_site_scope()->call(
18597 "/addons/{$this->_plugin->id}/installs.json",
18598 'post',
18599 $params
18600 );
18601
18602 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
18603 if ( is_object( $bundle_license ) ) {
18604 /**
18605 * 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.
18606 *
18607 * @author Leo Fajardo (@leorw)
18608 * @since 2.4.0
18609 */
18610 } else {
18611 $error_message = FS_Api::is_api_error_object( $result ) ?
18612 $result->error->message :
18613 $this->get_text_inline( 'An unknown error has occurred.', 'unknown-error' );
18614
18615 $this->_admin_notices->add(
18616 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
18617 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $error_message . '</b>',
18618 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
18619 'error'
18620 );
18621 }
18622
18623 return;
18624 }
18625
18626 $addon_installs = $result->installs;
18627 foreach ( $addon_installs as $key => $addon_install ) {
18628 $addon_installs[ $key ] = new FS_Site( $addon_install );
18629 }
18630
18631 $first_install = $addon_installs[0];
18632
18633 // Get user information based on parent's plugin.
18634 $user = $parent_fs->get_user();
18635
18636 // First of all, set site and user info - otherwise we won't
18637 // be able to invoke API calls.
18638 $this->_site = $first_install;
18639 $this->_user = $user;
18640
18641 // Sync add-on plans.
18642 $this->_sync_plans();
18643
18644 $this->handle_account_connection( $addon_installs, ! fs_is_network_admin() );
18645
18646 // Get site's current plan.
18647 //$this->_site->plan = $this->_get_plan_by_id( $this->_site->plan->id );
18648
18649 // Sync licenses.
18650 $this->_sync_licenses();
18651
18652 if ( ! fs_is_network_admin() ) {
18653 // Try to activate premium license.
18654 $this->_activate_license( true, $bundle_license );
18655
18656 if ( is_object( $bundle_license ) ) {
18657 $this->maybe_activate_bundle_license( $bundle_license );
18658 }
18659 } else {
18660 if ( is_object( $bundle_license ) ) {
18661 $premium_license = $bundle_license;
18662 } else {
18663 $license_id = fs_request_get( 'license_id' );
18664
18665 if ( is_object( $this->_site ) &&
18666 FS_Plugin_License::is_valid_id( $license_id ) &&
18667 $license_id == $this->_site->license_id
18668 ) {
18669 // License is already activated.
18670 return;
18671 }
18672
18673 $premium_license = FS_Plugin_License::is_valid_id( $license_id ) ?
18674 $this->_get_license_by_id( $license_id ) :
18675 $this->_get_available_premium_license();
18676 }
18677
18678 if ( is_object( $premium_license ) ) {
18679 $this->maybe_network_activate_addon_license( $premium_license );
18680 }
18681 }
18682 }
18683
18684 /**
18685 * @author Leo Fajardo (@leorw)
18686 * @since 2.3.0
18687 *
18688 * @param FS_Site[] $installs
18689 * @param bool $is_site_level
18690 */
18691 private function handle_account_connection( $installs, $is_site_level ) {
18692 $first_install = $installs[0];
18693
18694 if ( $is_site_level ) {
18695 $this->_set_account( $this->_user, $first_install );
18696
18697 $this->do_action( 'after_account_connection', $this->_user, $first_install );
18698 } else {
18699 $this->_store_user();
18700
18701 // Map site addresses to their blog IDs.
18702 $address_to_blog_map = $this->get_address_to_blog_map();
18703
18704 $first_blog_id = null;
18705 $blog_2_install_map = array();
18706 foreach ( $installs as $install ) {
18707 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
18708 $blog_id = $address_to_blog_map[ $address ];
18709
18710 $this->_store_site( true, $blog_id, $install );
18711
18712 if ( is_null( $first_blog_id ) ) {
18713 $first_blog_id = $blog_id;
18714 }
18715
18716 $blog_2_install_map[ $blog_id ] = $install;
18717 }
18718
18719 if ( ! FS_User::is_valid_id( $this->_storage->network_user_id ) ||
18720 ! is_object( self::_get_user_by_id( $this->_storage->network_user_id ) )
18721 ) {
18722 // Store network user.
18723 $this->_storage->network_user_id = $this->_user->id;
18724 }
18725
18726 if ( ! FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ) {
18727 $this->_storage->network_install_blog_id = $first_blog_id;
18728 }
18729
18730 if ( count( $installs ) === count( $address_to_blog_map ) ) {
18731 // Super admin opted in for all sites in the network.
18732 $this->_storage->is_network_connected = true;
18733 }
18734
18735 $this->_store_licenses( false );
18736
18737 self::$_accounts->store();
18738
18739 // Don't sync the installs data on network upgrade
18740 if ( ! $this->network_upgrade_mode_completed() ) {
18741 $this->send_installs_update();
18742 }
18743
18744 $current_blog = get_current_blog_id();
18745
18746 foreach ( $blog_2_install_map as $blog_id => $install ) {
18747 $this->switch_to_blog( $blog_id );
18748
18749 $this->do_action( 'after_account_connection', $this->_user, $install );
18750 }
18751
18752 // Switch install context back to the first install.
18753 $this->switch_to_blog(
18754 $current_blog,
18755 $first_install,
18756 ( $this->_site->id != $first_install->id )
18757 );
18758
18759 $this->do_action( 'after_network_account_connection', $this->_user, $blog_2_install_map );
18760 }
18761 }
18762
18763 /**
18764 * Tries to activate parent account based on add-on's info.
18765 *
18766 * @author Vova Feldman (@svovaf)
18767 * @since 1.2.2.7
18768 *
18769 * @param Freemius $parent_fs
18770 */
18771 private function activate_parent_account( Freemius $parent_fs ) {
18772 if ( ! $this->is_addon() ) {
18773 // This is not an add-on.
18774 return;
18775 }
18776
18777 if ( $parent_fs->is_registered() ) {
18778 // Already activated.
18779 return;
18780 }
18781
18782 // Activate parent with add-on's user credentials.
18783 $parent_install = $this->get_api_user_scope()->call(
18784 "/plugins/{$parent_fs->_plugin->id}/installs.json",
18785 'post',
18786 $parent_fs->get_install_data_for_api( array(
18787 'uid' => $parent_fs->get_anonymous_id(),
18788 ), false, false )
18789 );
18790
18791 if ( isset( $parent_install->error ) ) {
18792 $this->_admin_notices->add(
18793 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
18794 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $parent_install->error->message . '</b>',
18795 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
18796 'error'
18797 );
18798
18799 return;
18800 }
18801
18802 $parent_fs->_admin_notices->remove_sticky( 'connect_account' );
18803
18804 if ( $parent_fs->is_pending_activation() ) {
18805 $parent_fs->clear_pending_activation_mode();
18806 }
18807
18808 // Get user information based on parent's plugin.
18809 $user = $this->get_user();
18810
18811 // First of all, set site info - otherwise we won't
18812 // be able to invoke API calls.
18813 $parent_fs->_site = new FS_Site( $parent_install );
18814 $parent_fs->_user = $user;
18815
18816 // Sync add-on plans.
18817 $parent_fs->_sync_plans();
18818
18819 $parent_fs->_set_account( $user, $parent_fs->_site );
18820 }
18821
18822 #endregion
18823
18824 #----------------------------------------------------------------------------------
18825 #region Admin Menu Items
18826 #----------------------------------------------------------------------------------
18827
18828 private $_menu_items = array();
18829
18830 /**
18831 * @author Vova Feldman (@svovaf)
18832 * @since 1.2.1.8
18833 *
18834 * @return array
18835 */
18836 function get_menu_items() {
18837 return $this->_menu_items;
18838 }
18839
18840 /**
18841 * @author Vova Feldman (@svovaf)
18842 * @since 1.0.7
18843 *
18844 * @return string
18845 */
18846 function get_menu_slug() {
18847 return $this->_menu->get_slug();
18848 }
18849
18850 /**
18851 * @author Vova Feldman (@svovaf)
18852 * @since 1.0.9
18853 */
18854 function _prepare_admin_menu() {
18855 // if ( ! $this->is_on() ) {
18856 // return;
18857 // }
18858
18859 if ( is_object( $this->_site ) && ! $this->is_registered() ) {
18860 return;
18861 }
18862
18863 /**
18864 * When running from a site admin with a network activated module and the connection
18865 * was NOT delegated and the user still haven't skipped or opted-in, then hide the
18866 * site level settings.
18867 *
18868 * @author Vova Feldman (@svovaf)
18869 * @since 2.0.0
18870 */
18871 $should_hide_site_admin_settings = (
18872 $this->_is_network_active &&
18873 ! fs_is_network_admin() &&
18874 ! $this->is_delegated_connection() &&
18875 ! $this->is_anonymous() &&
18876 ! $this->is_registered()
18877 );
18878
18879 $should_hide_site_admin_settings = $this->apply_filters( 'should_hide_site_admin_settings_on_network_activation_mode', $should_hide_site_admin_settings );
18880
18881 if ( ( ! $this->has_api_connectivity() && ! $this->is_enable_anonymous() ) ||
18882 $should_hide_site_admin_settings
18883 ) {
18884 $this->_menu->remove_menu_item( $should_hide_site_admin_settings );
18885 } else {
18886 $this->do_action( fs_is_network_admin() ?
18887 'before_network_admin_menu_init' :
18888 'before_admin_menu_init'
18889 );
18890
18891 $this->add_menu_action();
18892
18893 $this->add_network_menu_when_missing();
18894
18895 $this->add_submenu_items();
18896 }
18897 }
18898
18899 /**
18900 * Admin dashboard menu items modifications.
18901 *
18902 * NOTE: admin_menu action executed before admin_init.
18903 *
18904 * @author Vova Feldman (@svovaf)
18905 * @since 1.0.7
18906 *
18907 */
18908 private function add_menu_action() {
18909 if ( $this->is_activation_mode() ) {
18910 if ( $this->show_opt_in_on_setting_page() ) {
18911 $this->override_plugin_menu_with_activation();
18912 } else {
18913 /**
18914 * Handle theme opt-in when the opt-in form shows as a dialog box in the themes page.
18915 */
18916 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) ) {
18917 add_action( 'load-themes.php', array( &$this, '_install_with_current_user' ) );
18918 } else if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ||
18919 fs_request_get_bool( 'pending_activation' )
18920 ) {
18921 add_action( 'load-themes.php', array( &$this, '_install_with_new_user' ) );
18922 }
18923 }
18924 } else {
18925 if ( ! $this->is_registered() ) {
18926 // If not registered try to install user.
18927 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) {
18928 $this->_install_with_new_user();
18929 }
18930 } else if (
18931 fs_request_is_action( 'sync_user' ) &&
18932 ( ! $this->has_settings_menu() || $this->show_opt_in_on_themes_page() )
18933 ) {
18934 $this->_handle_account_user_sync();
18935 }
18936 }
18937 }
18938
18939 /**
18940 * @author Vova Feldman (@svovaf)
18941 * @since 1.0.1
18942 */
18943 function _redirect_on_clicked_menu_link() {
18944 $this->_logger->entrance();
18945
18946 $page = fs_request_get('page');
18947 $page = is_string($page) ? strtolower($page) : '';
18948
18949 $this->_logger->log( 'page = ' . $page );
18950
18951 foreach ( $this->_menu_items as $priority => $items ) {
18952 foreach ( $items as $item ) {
18953 if ( isset( $item['url'] ) ) {
18954 if ( $page === $this->_menu->get_slug( strtolower( $item['menu_slug'] ) ) ) {
18955 $this->_logger->log( 'Redirecting to ' . $item['url'] );
18956
18957 fs_redirect( $item['url'] );
18958 }
18959 }
18960 }
18961 }
18962 }
18963
18964 /**
18965 * Remove plugin's all admin menu items & pages, and replace with activation page.
18966 *
18967 * @author Vova Feldman (@svovaf)
18968 * @since 1.0.1
18969 */
18970 private function override_plugin_menu_with_activation() {
18971 $this->_logger->entrance();
18972
18973 $hook = false;
18974
18975 if ( ! $this->has_settings_menu() ) {
18976 // Add the opt-in page without a menu item.
18977 $hook = FS_Admin_Menu_Manager::add_subpage(
18978 '',
18979 $this->get_plugin_name(),
18980 $this->get_plugin_name(),
18981 'manage_options',
18982 $this->_slug,
18983 array( &$this, '_connect_page_render' )
18984 );
18985 } else if ( $this->_menu->is_top_level() ) {
18986 if ( $this->_menu->is_override_exact() ) {
18987 // Make sure the current page is matching the activation page.
18988 if ( ! $this->is_matching_url( $this->get_activation_url() ) ) {
18989 return;
18990 }
18991 }
18992
18993 $hook = $this->_menu->override_menu_item( array( &$this, '_connect_page_render' ) );
18994
18995 if ( false === $hook ) {
18996 // Create new menu item just for the opt-in.
18997 $hook = FS_Admin_Menu_Manager::add_page(
18998 $this->get_plugin_name(),
18999 $this->get_plugin_name(),
19000 'manage_options',
19001 $this->_menu->get_slug(),
19002 array( &$this, '_connect_page_render' )
19003 );
19004 }
19005 } else {
19006 $menus = array( $this->_menu->get_parent_slug() );
19007
19008 if ( $this->_menu->is_override_exact() ) {
19009 // Make sure the current page is matching the activation page.
19010 if ( ! $this->is_matching_url( $this->get_activation_url() ) ) {
19011 return;
19012 }
19013 }
19014
19015 foreach ( $menus as $parent_slug ) {
19016 $hook = $this->_menu->override_submenu_action(
19017 $parent_slug,
19018 $this->_menu->get_raw_slug(),
19019 array( &$this, '_connect_page_render' )
19020 );
19021
19022 if ( false !== $hook ) {
19023 // Found plugin's submenu item.
19024 break;
19025 }
19026 }
19027 }
19028
19029 if ( $this->is_activation_page() ) {
19030 // Clean admin page from distracting content.
19031 self::_clean_admin_content_section();
19032 }
19033
19034 if ( false !== $hook ) {
19035 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) ) {
19036 $this->_install_with_current_user();
19037 } else if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) {
19038 $this->_install_with_new_user();
19039 }
19040 }
19041 }
19042
19043 /**
19044 * If a plugin was network activated and connected but don't have a network
19045 * level settings, then add an artificial menu item for the Account and other
19046 * Freemius settings.
19047 *
19048 * @author Vova Feldman (@svovaf)
19049 * @since 2.0.0
19050 */
19051 private function add_network_menu_when_missing() {
19052 $this->_logger->entrance();
19053
19054 if ( ! $this->_is_network_active ) {
19055 // Plugin wasn't activated on the network level.
19056 return;
19057 }
19058
19059 if ( ! fs_is_network_admin() ) {
19060 // The context is not the network admin.
19061 return;
19062 }
19063
19064 if ( $this->_menu->has_network_menu() ) {
19065 // Plugin already has a network level menu.
19066 return;
19067 }
19068
19069 if ( $this->is_network_activation_mode() ) {
19070 /**
19071 * Do not add during activation mode, otherwise, there will be duplicate menus while the opt-in
19072 * screen is being shown.
19073 *
19074 * @author Leo Fajardo (@leorw)
19075 */
19076 return;
19077 }
19078
19079 if ( ! WP_FS__SHOW_NETWORK_EVEN_WHEN_DELEGATED ) {
19080 if ( $this->is_network_delegated_connection() ) {
19081 // Super-admin delegated the connection to the site admins.
19082 return;
19083 }
19084 }
19085
19086 if ( ! $this->_menu->has_menu() || $this->_menu->is_top_level() ) {
19087
19088 if ( $this->_menu->has_menu() ||
19089 ! $this->is_addon() ||
19090 $this->is_activation_mode()
19091 ) {
19092 $this->_dynamically_added_top_level_page_hook_name = $this->_menu->add_page_and_update(
19093 $this->get_plugin_name(),
19094 $this->get_plugin_name(),
19095 'manage_options',
19096 $this->_menu->has_menu() ? $this->_menu->get_slug() : $this->_slug
19097 );
19098 }
19099 } else {
19100 $this->_menu->add_subpage_and_update(
19101 $this->_menu->get_parent_slug(),
19102 $this->get_plugin_name(),
19103 $this->get_plugin_name(),
19104 'manage_options',
19105 $this->_menu->get_slug()
19106 );
19107 }
19108 }
19109
19110 /**
19111 * @author Leo Fajardo (@leorw)
19112 * @since 1.2.1
19113 *
19114 * return string
19115 */
19116 function get_top_level_menu_capability() {
19117 global $menu;
19118
19119 $top_level_menu_slug = $this->get_top_level_menu_slug();
19120
19121 foreach ( $menu as $menu_info ) {
19122 /**
19123 * The second element in the menu info array is the capability/role that has access to the menu and the
19124 * third element is the menu slug.
19125 */
19126 if ( $menu_info[2] === $top_level_menu_slug ) {
19127 return $menu_info[1];
19128 }
19129 }
19130
19131 return 'read';
19132 }
19133
19134 /**
19135 * @author Vova Feldman (@svovaf)
19136 * @since 1.0.0
19137 *
19138 * @return string
19139 */
19140 private function get_top_level_menu_slug() {
19141 return ( $this->is_addon() ?
19142 $this->get_parent_instance()->_menu->get_top_level_menu_slug() :
19143 $this->_menu->get_top_level_menu_slug() );
19144 }
19145
19146 /**
19147 * @author Vova Feldman (@svovaf)
19148 * @since 1.2.2.7
19149 *
19150 * @return string
19151 */
19152 function get_pricing_cta_label() {
19153 $label = $this->get_text_inline( 'Upgrade', 'upgrade' );
19154
19155 if ( $this->is_in_trial_promotion() &&
19156 ! $this->is_paying_or_trial()
19157 ) {
19158 // If running a trial promotion, modify the pricing to load the trial.
19159 $label = $this->get_text_inline( 'Start Trial', 'start-trial' );
19160 } else if ( $this->is_paying() ) {
19161 $label = $this->get_text_inline( 'Pricing', 'pricing' );
19162 }
19163
19164 return $label;
19165 }
19166
19167 /**
19168 * @author Vova Feldman (@svovaf)
19169 * @since 1.2.2.7
19170 *
19171 * @return bool
19172 */
19173 function is_pricing_page_visible() {
19174 return (
19175 // Has at least one paid plan.
19176 $this->has_paid_plan() &&
19177 // Didn't ask to hide the pricing page.
19178 $this->is_page_visible( 'pricing' ) &&
19179 // Don't have a valid active license or has more than one plan.
19180 ( ! $this->is_paying() || ! $this->is_single_plan( true ) )
19181 );
19182 }
19183
19184 /**
19185 * @author Leo Fajardo (@leorw)
19186 * @since 2.3.0
19187 *
19188 * @param bool $is_activation_mode
19189 *
19190 * @return bool
19191 */
19192 private function should_add_submenu_or_action_links( $is_activation_mode ) {
19193 if ( $this->is_addon() ) {
19194 // No submenu items or action links for add-ons.
19195 return false;
19196 }
19197
19198 if ( $this->show_opt_in_on_themes_page() ) {
19199 if ( ! fs_is_network_admin() ) {
19200 // Also add action links or submenu items when running in a free .org theme so the tabs will be visible.
19201 return true;
19202 }
19203 } else if ( $is_activation_mode ) {
19204 // Don't show submenu-items/tabs in activation mode, unless it's a wp.org theme.
19205 return false;
19206 }
19207
19208 if ( fs_is_network_admin() ) {
19209 /**
19210 * Add submenu items or action links to network level when plugin was network activated and the super
19211 * admin did NOT delegate the connection of all sites to site admins.
19212 */
19213 return (
19214 $this->_is_network_active &&
19215 ( WP_FS__SHOW_NETWORK_EVEN_WHEN_DELEGATED ||
19216 ! $this->is_network_delegated_connection() )
19217 );
19218 }
19219
19220 return ( ! $this->_is_network_active || $this->is_delegated_connection() );
19221 }
19222
19223 /**
19224 * Add default Freemius menu items.
19225 *
19226 * @author Vova Feldman (@svovaf)
19227 * @since 1.0.0
19228 * @since 1.2.2.7 Also add submenu items when running in a free .org theme so the tabs will be visible.
19229 */
19230 private function add_submenu_items() {
19231 $this->_logger->entrance();
19232
19233 $is_activation_mode = $this->is_activation_mode();
19234
19235 $add_submenu_items = $this->should_add_submenu_or_action_links( $is_activation_mode );
19236
19237 if ( $add_submenu_items ) {
19238 if ( $this->has_affiliate_program() ) {
19239 // Add affiliation page.
19240 $this->add_submenu_item(
19241 $this->get_text_inline( 'Affiliation', 'affiliation' ),
19242 array( &$this, '_affiliation_page_render' ),
19243 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Affiliation', 'affiliation' ),
19244 'manage_options',
19245 'affiliation',
19246 'Freemius::_clean_admin_content_section',
19247 WP_FS__DEFAULT_PRIORITY,
19248 $this->is_submenu_item_visible( 'affiliation' )
19249 );
19250 }
19251 }
19252
19253 if ( $add_submenu_items ||
19254 ( $is_activation_mode &&
19255 $this->is_only_premium() &&
19256 $this->is_admin_page( 'account' ) &&
19257 fs_request_is_action( $this->get_unique_affix() . '_sync_license' )
19258 )
19259 ) {
19260 if ( ! WP_FS__DEMO_MODE && $this->is_registered() ) {
19261 $show_account = (
19262 $this->is_submenu_item_visible( 'account' ) &&
19263 /**
19264 * @since 1.2.2.7 Don't show the Account for free WP.org themes without any paid plans.
19265 */
19266 ( ! $this->is_free_wp_org_theme() || $this->has_paid_plan() )
19267 );
19268
19269 // Add user account page.
19270 $this->add_submenu_item(
19271 $this->get_text_inline( 'Account', 'account' ),
19272 array( &$this, '_account_page_render' ),
19273 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Account', 'account' ),
19274 'manage_options',
19275 'account',
19276 array( &$this, '_account_page_load' ),
19277 WP_FS__DEFAULT_PRIORITY,
19278 ( $add_submenu_items && $show_account )
19279 );
19280 }
19281 }
19282
19283 if ( $add_submenu_items ) {
19284 if (! WP_FS__DEMO_MODE && ! $this->is_whitelabeled() ) {
19285 // Add contact page.
19286 $this->add_submenu_item(
19287 $this->get_text_inline( 'Contact Us', 'contact-us' ),
19288 array( &$this, '_contact_page_render' ),
19289 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Contact Us', 'contact-us' ),
19290 'manage_options',
19291 'contact',
19292 'Freemius::_clean_admin_content_section',
19293 WP_FS__DEFAULT_PRIORITY,
19294 $this->is_submenu_item_visible( 'contact' )
19295 );
19296 }
19297
19298 if ( $this->has_addons() ) {
19299 $this->add_submenu_item(
19300 $this->get_text_inline( 'Add-Ons', 'add-ons' ),
19301 array( &$this, '_addons_page_render' ),
19302 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Add-Ons', 'add-ons' ),
19303 'manage_options',
19304 'addons',
19305 array( &$this, '_addons_page_load' ),
19306 WP_FS__LOWEST_PRIORITY - 1,
19307 $this->is_submenu_item_visible( 'addons' )
19308 );
19309 }
19310 }
19311
19312 if ( $add_submenu_items ||
19313 ( $is_activation_mode && $this->is_only_premium() && $this->is_admin_page( 'pricing' ) )
19314 ) {
19315 if (! WP_FS__DEMO_MODE && ! $this->is_whitelabeled() ) {
19316 $show_pricing = (
19317 $this->is_submenu_item_visible( 'pricing' ) &&
19318 $this->is_pricing_page_visible()
19319 );
19320
19321 $pricing_cta_text = $this->get_pricing_cta_label();
19322 $pricing_class = 'upgrade-mode';
19323 if ( $show_pricing ) {
19324 if ( $this->is_in_trial_promotion() &&
19325 ! $this->is_paying_or_trial()
19326 ) {
19327 // If running a trial promotion, modify the pricing to load the trial.
19328 $pricing_class = 'trial-mode';
19329 } else if ( $this->is_paying() ) {
19330 $pricing_class = '';
19331 }
19332 }
19333
19334 // Add upgrade/pricing page.
19335 $this->add_submenu_item(
19336 $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' ) ),
19337 array( &$this, '_pricing_page_render' ),
19338 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_x_inline( 'Pricing', 'noun', 'pricing' ),
19339 'manage_options',
19340 'pricing',
19341 'Freemius::_clean_admin_content_section',
19342 WP_FS__LOWEST_PRIORITY,
19343 ( $add_submenu_items && $show_pricing ),
19344 $pricing_class
19345 );
19346 }
19347 }
19348
19349 if ( ! $is_activation_mode || ( true !== $this->_storage->require_license_activation ) ) {
19350 /**
19351 * Add the other menu items if there are any when not in activation mode or license activation is not
19352 * required (license activation is required for registered or anonymous users after activating the
19353 * premium version when the site is not in trial mode or there's no active valid license).
19354 *
19355 * @author Leo Fajardo (@leorw)
19356 * @since 2.2.1
19357 */
19358 if ( 0 < count( $this->_menu_items ) ) {
19359 if ( ! $this->_menu->is_top_level() ) {
19360 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
19361
19362 // Append submenu items right after the plugin's submenu item.
19363 $this->order_sub_submenu_items();
19364 } else {
19365 // Append submenu items.
19366 $this->embed_submenu_items();
19367 }
19368 }
19369 }
19370 }
19371
19372 /**
19373 * Moved the actual submenu item additions to a separated function,
19374 * in order to support sub-submenu items when the plugin's settings
19375 * only have a submenu and not top-level menu item.
19376 *
19377 * @author Vova Feldman (@svovaf)
19378 * @since 1.1.4
19379 */
19380 private function embed_submenu_items() {
19381 $item_template = $this->_menu->is_top_level() ?
19382 '<span class="fs-submenu-item %s %s %s">%s</span>' :
19383 '<span class="fs-submenu-item fs-sub %s %s %s">%s</span>';
19384
19385 $top_level_menu_capability = $this->get_top_level_menu_capability();
19386
19387 ksort( $this->_menu_items );
19388
19389 $is_first_submenu_item = true;
19390
19391 foreach ( $this->_menu_items as $priority => $items ) {
19392 foreach ( $items as $item ) {
19393 $capability = ( ! empty( $item['capability'] ) ? $item['capability'] : $top_level_menu_capability );
19394
19395 $menu_item = sprintf(
19396 $item_template,
19397 $this->get_unique_affix(),
19398 $item['menu_slug'],
19399 ! empty( $item['class'] ) ? $item['class'] : '',
19400 $item['menu_title']
19401 );
19402
19403 $top_level_menu_slug = $this->get_top_level_menu_slug();
19404 $menu_slug = $this->_menu->get_slug( $item['menu_slug'] );
19405
19406 if ( ! isset( $item['url'] ) ) {
19407 $hook = FS_Admin_Menu_Manager::add_subpage(
19408 $item['show_submenu'] ?
19409 $top_level_menu_slug :
19410 '',
19411 $item['page_title'],
19412 $menu_item,
19413 $capability,
19414 $menu_slug,
19415 $item['render_function']
19416 );
19417
19418 if ( false !== $item['before_render_function'] ) {
19419 add_action( "load-$hook", $item['before_render_function'] );
19420 }
19421 } else {
19422 FS_Admin_Menu_Manager::add_subpage(
19423 $item['show_submenu'] ?
19424 $top_level_menu_slug :
19425 '',
19426 $item['page_title'],
19427 $menu_item,
19428 $capability,
19429 $menu_slug,
19430 array( $this, '' )
19431 );
19432 }
19433
19434 if ( $item['show_submenu'] && $is_first_submenu_item ) {
19435 if ( $this->_is_network_active && ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
19436 /**
19437 * If the top-level menu has been dynamically created, remove the first submenu item that
19438 * WordPress automatically creates when there's no submenu item whose slug matches the
19439 * parent's. In the following example, the `Awesome Plugin` submenu item will be removed.
19440 *
19441 * Awesome Plugin
19442 * - Awesome Plugin <-- we want to remove this since there's no real setting page for the top-level
19443 *
19444 * @author Leo Fajardo (@leorw)
19445 */
19446 remove_submenu_page( $top_level_menu_slug, $top_level_menu_slug );
19447 }
19448
19449 $is_first_submenu_item = false;
19450 }
19451 }
19452 }
19453 }
19454
19455 /**
19456 * Re-order the submenu items so all Freemius added new submenu items
19457 * are added right after the plugin's settings submenu item.
19458 *
19459 * @author Vova Feldman (@svovaf)
19460 * @since 1.1.4
19461 */
19462 private function order_sub_submenu_items() {
19463 global $submenu;
19464
19465 $menu_slug = $this->_menu->get_top_level_menu_slug();
19466
19467 /**
19468 * Before "admin_menu" fires, WordPress will loop over the default submenus and remove pages for which the user
19469 * does not have permissions. So in case a plugin does not have top-level menu but does have submenus under any
19470 * of the default menus, only users that have the right role can access its sub-submenus (Account, Contact Us,
19471 * Support Forum, etc.) since $submenu[ $menu_slug ] will be empty if the user doesn't have permission.
19472 *
19473 * In case a plugin does not have submenus under any of the default menus but does have submenus under the menu
19474 * of another plugin, only users that have the right role can access its sub-submenus since we will use the
19475 * capability needed to access the parent menu as the capability for the submenus that we will add.
19476 */
19477 if ( empty( $submenu[ $menu_slug ] ) ) {
19478 return;
19479 }
19480
19481 $top_level_menu = &$submenu[ $menu_slug ];
19482
19483 $all_submenu_items_after = array();
19484
19485 $found_submenu_item = false;
19486
19487 foreach ( $top_level_menu as $submenu_id => $meta ) {
19488 if ( $found_submenu_item ) {
19489 // Remove all submenu items after the plugin's submenu item.
19490 $all_submenu_items_after[] = $meta;
19491 unset( $top_level_menu[ $submenu_id ] );
19492 }
19493
19494 if ( $this->_menu->get_raw_slug() === $meta[2] ) {
19495 // Found the submenu item, put all below.
19496 $found_submenu_item = true;
19497 continue;
19498 }
19499 }
19500
19501 // Embed all plugin's new submenu items.
19502 $this->embed_submenu_items();
19503
19504 // Start with specially high number to make sure it's appended.
19505 $i = max( 10000, max( array_keys( $top_level_menu ) ) + 1 );
19506 foreach ( $all_submenu_items_after as $meta ) {
19507 $top_level_menu[ $i ] = $meta;
19508 $i ++;
19509 }
19510
19511 // Sort submenu items.
19512 ksort( $top_level_menu );
19513 }
19514
19515 /**
19516 * Helper method to return the module's support forum URL.
19517 *
19518 * @author Vova Feldman (@svovaf)
19519 * @since 1.2.2.7
19520 *
19521 * @return string
19522 */
19523 function get_support_forum_url() {
19524 return $this->apply_filters( 'support_forum_url', "https://wordpress.org/support/{$this->_module_type}/{$this->_slug}" );
19525 }
19526
19527 /**
19528 * Displays the Support Forum link when enabled.
19529 *
19530 * Can be filtered like so:
19531 *
19532 * function _fs_show_support_menu( $is_visible, $menu_id ) {
19533 * if ( 'support' === $menu_id ) {
19534 * return _fs->is_registered();
19535 * }
19536 * return $is_visible;
19537 * }
19538 * _fs()->add_filter('is_submenu_visible', '_fs_show_support_menu', 10, 2);
19539 *
19540 */
19541 function _add_default_submenu_items() {
19542 if ( ! $this->is_on() ) {
19543 return;
19544 }
19545
19546 if ( ! $this->is_activation_mode() &&
19547 ( ( $this->_is_network_active && fs_is_network_admin() ) ||
19548 ( ! $this->_is_network_active && is_admin() ) )
19549 ) {
19550 $this->add_submenu_link_item(
19551 $this->apply_filters( 'support_forum_submenu', $this->get_text_inline( 'Support Forum', 'support-forum' ) ),
19552 $this->get_support_forum_url(),
19553 'wp-support-forum',
19554 null,
19555 50,
19556 $this->is_submenu_item_visible( 'support' )
19557 );
19558 }
19559 }
19560
19561 /**
19562 * @author Vova Feldman (@svovaf)
19563 * @since 1.0.1
19564 *
19565 * @param string $menu_title
19566 * @param callable $render_function
19567 * @param bool|string $page_title
19568 * @param string $capability
19569 * @param bool|string $menu_slug
19570 * @param bool|callable $before_render_function
19571 * @param int $priority
19572 * @param bool $show_submenu
19573 * @param string $class Since 1.2.1.5 can add custom classes to menu items.
19574 */
19575 function add_submenu_item(
19576 $menu_title,
19577 $render_function,
19578 $page_title = false,
19579 $capability = 'manage_options',
19580 $menu_slug = false,
19581 $before_render_function = false,
19582 $priority = WP_FS__DEFAULT_PRIORITY,
19583 $show_submenu = true,
19584 $class = ''
19585 ) {
19586 $this->_logger->entrance( 'Title = ' . $menu_title );
19587
19588 if ( $this->is_addon() ) {
19589 $parent_fs = $this->get_parent_instance();
19590
19591 if ( is_object( $parent_fs ) ) {
19592 $parent_fs->add_submenu_item(
19593 $menu_title,
19594 $render_function,
19595 $page_title,
19596 $capability,
19597 $menu_slug,
19598 $before_render_function,
19599 $priority,
19600 $show_submenu,
19601 $class
19602 );
19603
19604 return;
19605 }
19606 }
19607
19608 if ( ! isset( $this->_menu_items[ $priority ] ) ) {
19609 $this->_menu_items[ $priority ] = array();
19610 }
19611
19612 $this->_menu_items[ $priority ][] = array(
19613 'page_title' => is_string( $page_title ) ? $page_title : $menu_title,
19614 'menu_title' => $menu_title,
19615 'capability' => $capability,
19616 'menu_slug' => is_string( $menu_slug ) ? $menu_slug : strtolower( $menu_title ),
19617 'render_function' => $render_function,
19618 'before_render_function' => $before_render_function,
19619 'show_submenu' => $show_submenu,
19620 'class' => $class,
19621 );
19622 }
19623
19624 /**
19625 * @author Vova Feldman (@svovaf)
19626 * @since 1.0.1
19627 *
19628 * @param string $menu_title
19629 * @param string $url
19630 * @param bool $menu_slug
19631 * @param string $capability
19632 * @param int $priority
19633 * @param bool $show_submenu
19634 */
19635 function add_submenu_link_item(
19636 $menu_title,
19637 $url,
19638 $menu_slug = false,
19639 $capability = 'read',
19640 $priority = WP_FS__DEFAULT_PRIORITY,
19641 $show_submenu = true
19642 ) {
19643 $this->_logger->entrance( 'Title = ' . $menu_title . '; Url = ' . $url );
19644
19645 if ( $this->is_addon() ) {
19646 $parent_fs = $this->get_parent_instance();
19647
19648 if ( is_object( $parent_fs ) ) {
19649 $parent_fs->add_submenu_link_item(
19650 $menu_title,
19651 $url,
19652 $menu_slug,
19653 $capability,
19654 $priority,
19655 $show_submenu
19656 );
19657
19658 return;
19659 }
19660 }
19661
19662 if ( ! isset( $this->_menu_items[ $priority ] ) ) {
19663 $this->_menu_items[ $priority ] = array();
19664 }
19665
19666 $this->_menu_items[ $priority ][] = array(
19667 'menu_title' => $menu_title,
19668 'capability' => $capability,
19669 'menu_slug' => is_string( $menu_slug ) ? $menu_slug : strtolower( $menu_title ),
19670 'url' => $url,
19671 'page_title' => $menu_title,
19672 'render_function' => 'fs_dummy',
19673 'before_render_function' => '',
19674 'show_submenu' => $show_submenu,
19675 );
19676 }
19677
19678 #endregion ------------------------------------------------------------------
19679
19680 #--------------------------------------------------------------------------------
19681 #region Admin Notices
19682 #--------------------------------------------------------------------------------
19683
19684 /**
19685 * @author Vova Feldman (@svovaf)
19686 * @since 2.3.1
19687 *
19688 * @param string|string[] $ids
19689 * @param int|null $network_level_or_blog_id
19690 *
19691 * @uses FS_Admin_Notices::remove_sticky()
19692 */
19693 function remove_sticky( $ids, $network_level_or_blog_id = null ) {
19694 $this->_admin_notices->remove_sticky( $ids, $network_level_or_blog_id );
19695 }
19696
19697 #endregion
19698
19699 #--------------------------------------------------------------------------------
19700 #region Actions / Hooks / Filters
19701 #--------------------------------------------------------------------------------
19702
19703 /**
19704 * @author Vova Feldman (@svovaf)
19705 * @since 1.1.7
19706 *
19707 * @param string $tag
19708 *
19709 * @return string
19710 */
19711 public function get_action_tag( $tag ) {
19712 return self::get_action_tag_static( $tag, $this->_slug, $this->is_plugin() );
19713 }
19714
19715 /**
19716 * @author Vova Feldman (@svovaf)
19717 * @since 1.2.1.6
19718 *
19719 * @param string $tag
19720 * @param string $slug
19721 * @param bool $is_plugin
19722 *
19723 * @return string
19724 */
19725 static function get_action_tag_static( $tag, $slug = '', $is_plugin = true ) {
19726 $action = "fs_{$tag}";
19727
19728 if ( ! empty( $slug ) ) {
19729 $action .= '_' . self::get_module_unique_affix( $slug, $is_plugin );
19730 }
19731
19732 return $action;
19733 }
19734
19735 /**
19736 * Returns a string that can be used to generate a unique action name,
19737 * option name, HTML element ID, or HTML element class.
19738 *
19739 * @author Leo Fajardo (@leorw)
19740 * @since 1.2.2
19741 *
19742 * @return string
19743 */
19744 public function get_unique_affix() {
19745 return self::get_module_unique_affix( $this->_slug, $this->is_plugin() );
19746 }
19747
19748 /**
19749 * Returns a string that can be used to generate a unique action name,
19750 * option name, HTML element ID, or HTML element class.
19751 *
19752 * @author Vova Feldman (@svovaf)
19753 * @since 1.2.2.5
19754 *
19755 * @param string $slug
19756 * @param bool $is_plugin
19757 *
19758 * @return string
19759 */
19760 static function get_module_unique_affix( $slug, $is_plugin = true ) {
19761 $affix = $slug;
19762
19763 if ( ! $is_plugin ) {
19764 $affix .= '-' . WP_FS__MODULE_TYPE_THEME;
19765 }
19766
19767 return $affix;
19768 }
19769
19770 /**
19771 * @author Vova Feldman (@svovaf)
19772 * @since 1.2.1
19773 * @since 1.2.2.5 The AJAX action names are based on the module ID, not like the non-AJAX actions that are
19774 * based on the slug for backward compatibility.
19775 *
19776 * @param string $tag
19777 *
19778 * @return string
19779 */
19780 function get_ajax_action( $tag ) {
19781 return self::get_ajax_action_static( $tag, $this->_module_id );
19782 }
19783
19784 /**
19785 * @author Vova Feldman (@svovaf)
19786 * @since 1.2.1.7
19787 *
19788 * @param string $tag
19789 *
19790 * @return string
19791 */
19792 function get_ajax_security( $tag ) {
19793 return wp_create_nonce( $this->get_ajax_action( $tag ) );
19794 }
19795
19796 /**
19797 * @author Vova Feldman (@svovaf)
19798 * @since 1.2.1.7
19799 *
19800 * @param string $tag
19801 */
19802 function check_ajax_referer( $tag ) {
19803 check_ajax_referer( $this->get_ajax_action( $tag ), 'security' );
19804 }
19805
19806 /**
19807 * @author Vova Feldman (@svovaf)
19808 * @since 1.2.1.6
19809 * @since 1.2.2.5 The AJAX action names are based on the module ID, not like the non-AJAX actions that are
19810 * based on the slug for backward compatibility.
19811 *
19812 * @param string $tag
19813 * @param number|null $module_id
19814 *
19815 * @return string
19816 */
19817 static function get_ajax_action_static( $tag, $module_id = null ) {
19818 $action = "fs_{$tag}";
19819
19820 if ( ! empty( $module_id ) ) {
19821 $action .= "_{$module_id}";
19822 }
19823
19824 return $action;
19825 }
19826
19827 /**
19828 * Do action, specific for the current context plugin.
19829 *
19830 * @author Vova Feldman (@svovaf)
19831 * @since 1.0.1
19832 *
19833 * @param string $tag The name of the action to be executed.
19834 * @param mixed $arg,... Optional. Additional arguments which are passed on to the
19835 * functions hooked to the action. Default empty.
19836 *
19837 * @uses do_action()
19838 */
19839 function do_action( $tag, $arg = '' ) {
19840 $args = func_get_args();
19841
19842 $this->_logger->entrance( $tag );
19843
19844 call_user_func_array( 'do_action', array_merge(
19845 array( $this->get_action_tag( $tag ) ),
19846 array_slice( $args, 1 ) )
19847 );
19848 }
19849
19850 /**
19851 * Add action, specific for the current context plugin.
19852 *
19853 * @author Vova Feldman (@svovaf)
19854 * @since 1.0.1
19855 *
19856 * @param string $tag
19857 * @param callable $function_to_add
19858 * @param int $priority
19859 * @param int $accepted_args
19860 *
19861 * @uses add_action()
19862 */
19863 function add_action(
19864 $tag,
19865 $function_to_add,
19866 $priority = WP_FS__DEFAULT_PRIORITY,
19867 $accepted_args = 1
19868 ) {
19869 $this->_logger->entrance( $tag );
19870
19871 add_action( $this->get_action_tag( $tag ), $function_to_add, $priority, $accepted_args );
19872 }
19873
19874 /**
19875 * Add AJAX action, specific for the current context plugin.
19876 *
19877 * @author Vova Feldman (@svovaf)
19878 * @since 1.2.1
19879 *
19880 * @param string $tag
19881 * @param callable $function_to_add
19882 * @param int $priority
19883 *
19884 * @uses add_action()
19885 *
19886 * @return bool True if action added, false if no need to add the action since the AJAX call isn't matching.
19887 */
19888 function add_ajax_action(
19889 $tag,
19890 $function_to_add,
19891 $priority = WP_FS__DEFAULT_PRIORITY
19892 ) {
19893 $this->_logger->entrance( $tag );
19894
19895 return self::add_ajax_action_static(
19896 $tag,
19897 $function_to_add,
19898 $priority,
19899 $this->_module_id
19900 );
19901 }
19902
19903 /**
19904 * Add AJAX action.
19905 *
19906 * @author Vova Feldman (@svovaf)
19907 * @since 1.2.1.6
19908 *
19909 * @param string $tag
19910 * @param callable $function_to_add
19911 * @param int $priority
19912 * @param number|null $module_id
19913 *
19914 * @return bool True if action added, false if no need to add the action since the AJAX call isn't matching.
19915 * @uses add_action()
19916 *
19917 */
19918 static function add_ajax_action_static(
19919 $tag,
19920 $function_to_add,
19921 $priority = WP_FS__DEFAULT_PRIORITY,
19922 $module_id = null
19923 ) {
19924 self::$_static_logger->entrance( $tag );
19925
19926 if ( ! self::is_ajax_action_static( $tag, $module_id ) ) {
19927 return false;
19928 }
19929
19930 add_action(
19931 'wp_ajax_' . self::get_ajax_action_static( $tag, $module_id ),
19932 $function_to_add,
19933 $priority,
19934 0
19935 );
19936
19937 self::$_static_logger->info( "$tag AJAX callback action added." );
19938
19939 return true;
19940 }
19941
19942 /**
19943 * Send a JSON response back to an Ajax request.
19944 *
19945 * @author Vova Feldman (@svovaf)
19946 * @since 1.2.1.5
19947 *
19948 * @param mixed $response
19949 */
19950 static function shoot_ajax_response( $response ) {
19951 wp_send_json( $response );
19952 }
19953
19954 /**
19955 * Send a JSON response back to an Ajax request, indicating success.
19956 *
19957 * @author Vova Feldman (@svovaf)
19958 * @since 1.2.1.5
19959 *
19960 * @param mixed $data Data to encode as JSON, then print and exit.
19961 */
19962 static function shoot_ajax_success( $data = null ) {
19963 wp_send_json_success( $data );
19964 }
19965
19966 /**
19967 * Send a JSON response back to an Ajax request, indicating failure.
19968 *
19969 * @author Vova Feldman (@svovaf)
19970 * @since 1.2.1.5
19971 *
19972 * @param mixed $error Optional error message.
19973 */
19974 static function shoot_ajax_failure( $error = '' ) {
19975 $result = array( 'success' => false );
19976 if ( ! empty( $error ) ) {
19977 $result['error'] = $error;
19978 }
19979
19980 wp_send_json( $result );
19981 }
19982
19983 /**
19984 * Returns an AJAX URL with a special extra param to indicate whether the request was triggered from the network admin or blog admin.
19985 *
19986 * @author Vova Feldman (@svovaf)
19987 * @since 2.5.1
19988 *
19989 * @param string $wrap_with By default, returns the AJAX URL wrapped with single quotes.
19990 *
19991 * @return string
19992 */
19993 static function ajax_url( $wrap_with = "'") {
19994 if ( fs_is_network_admin() ) {
19995 $param_name = '_fs_network_admin';
19996 } else {
19997 $param_name = '_fs_blog_admin';
19998 }
19999
20000 $url = admin_url( 'admin-ajax.php', 'relative' );
20001 $url .= ( false === strpos( $url, '?' ) ) ? '?' : '&';
20002 $url .= "{$param_name}=true";
20003
20004 return "{$wrap_with}{$url}{$wrap_with}";
20005 }
20006
20007 /**
20008 * Apply filter, specific for the current context plugin.
20009 *
20010 * @author Vova Feldman (@svovaf)
20011 * @since 1.0.9
20012 *
20013 * @param string $tag The name of the filter hook.
20014 * @param mixed $value The value on which the filters hooked to `$tag` are applied on.
20015 *
20016 * @return mixed The filtered value after all hooked functions are applied to it.
20017 *
20018 * @uses apply_filters()
20019 */
20020 function apply_filters( $tag, $value ) {
20021 $args = func_get_args();
20022
20023 $this->_logger->entrance( $tag );
20024
20025 array_unshift( $args, $this->get_unique_affix() );
20026
20027 return call_user_func_array( 'fs_apply_filter', $args );
20028 }
20029
20030 /**
20031 * Add filter, specific for the current context plugin.
20032 *
20033 * @author Vova Feldman (@svovaf)
20034 * @since 1.0.9
20035 *
20036 * @param string $tag
20037 * @param callable $function_to_add
20038 * @param int $priority
20039 * @param int $accepted_args
20040 *
20041 * @uses add_filter()
20042 */
20043 function add_filter( $tag, $function_to_add, $priority = WP_FS__DEFAULT_PRIORITY, $accepted_args = 1 ) {
20044 $this->_logger->entrance( $tag );
20045
20046 add_filter( $this->get_action_tag( $tag ), $function_to_add, $priority, $accepted_args );
20047 }
20048
20049 /**
20050 * Check if has filter.
20051 *
20052 * @author Vova Feldman (@svovaf)
20053 * @since 1.1.4
20054 *
20055 * @param string $tag
20056 * @param callable|bool $function_to_check Optional. The callback to check for. Default false.
20057 *
20058 * @return false|int
20059 *
20060 * @uses has_filter()
20061 */
20062 function has_filter( $tag, $function_to_check = false ) {
20063 $this->_logger->entrance( $tag );
20064
20065 return has_filter( $this->get_action_tag( $tag ), $function_to_check );
20066 }
20067
20068 #endregion
20069
20070 /**
20071 * Override default i18n text phrases.
20072 *
20073 * @author Vova Feldman (@svovaf)
20074 * @since 1.1.6
20075 *
20076 * @param string[] string $key_value
20077 *
20078 * @uses fs_override_i18n()
20079 */
20080 function override_i18n( $key_value ) {
20081 fs_override_i18n( $key_value, $this->_slug );
20082 }
20083
20084 /* Account Page
20085 ------------------------------------------------------------------------------------------------------------------*/
20086 /**
20087 * Update site information.
20088 *
20089 * @author Vova Feldman (@svovaf)
20090 * @since 1.0.1
20091 *
20092 * @param bool $store Flush to Database if true.
20093 * @param null|int $network_level_or_blog_id Since 2.0.0
20094 * @param \FS_Site $site Since 2.0.0
20095 */
20096 private function _store_site( $store = true, $network_level_or_blog_id = null, FS_Site $site = null, $is_backup = false ) {
20097 $this->_logger->entrance();
20098
20099 if ( is_null( $site ) ) {
20100 $site = $this->_site;
20101 }
20102
20103 if ( !isset( $site ) || !is_object($site) || empty( $site->id ) ) {
20104 $this->_logger->error( "Empty install ID, can't store site." );
20105
20106 return;
20107 }
20108
20109 $site_clone = clone $site;
20110
20111 $sites = self::get_all_sites( $this->_module_type, $network_level_or_blog_id, $is_backup );
20112
20113 if (
20114 ! $is_backup &&
20115 is_object( $this->_user ) && $this->_user->id != $site->user_id
20116 ) {
20117 $this->sync_user_by_current_install( $site->user_id );
20118
20119 $prev_stored_user_id = $this->_storage->get( 'prev_user_id', false, $network_level_or_blog_id );
20120
20121 if ( empty( $prev_stored_user_id ) &&
20122 is_object($this->_user) && $this->_user->id != $site->user_id
20123 ) {
20124 /**
20125 * Store the current user ID as the previous user ID so that the previous user can be used
20126 * as the install's owner while the new owner's details are not yet available.
20127 *
20128 * This will be executed only in the `replica` site. For example, there are 2 sites, namely `original`
20129 * and `replica`, then an ownership change was initiated and completed in the `original`, the `replica`
20130 * will be using the previous user until it is updated again (e.g.: until the next clone of `original`
20131 * into `replica`.
20132 *
20133 * @author Leo Fajardo (@leorw)
20134 */
20135 $this->_storage->store( 'prev_user_id', $sites[ $this->_slug ]->user_id, $network_level_or_blog_id );
20136 }
20137 }
20138
20139 $sites[ $this->_slug ] = $site_clone;
20140
20141 $this->set_account_option(
20142 ( $is_backup ? 'prev_' : '' ) . 'sites',
20143 $sites,
20144 $store,
20145 $network_level_or_blog_id
20146 );
20147 }
20148
20149 /**
20150 * 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).
20151 *
20152 * @author Leo Fajardo (@leorw)
20153 * @since 2.5.0
20154 */
20155 private function back_up_site() {
20156 $this->_logger->entrance();
20157
20158 $site_clone = clone $this->_site;
20159
20160 $this->_store_site( true, null, $site_clone, true );
20161 }
20162
20163 /**
20164 * Update plugin's plans information.
20165 *
20166 * @author Vova Feldman (@svovaf)
20167 * @since 1.0.2
20168 *
20169 * @param bool $store Flush to Database if true.
20170 */
20171 private function _store_plans( $store = true ) {
20172 $this->_logger->entrance();
20173
20174 $plans = self::get_all_plans( $this->_module_type );
20175
20176 // Copy plans.
20177 $encrypted_plans = array();
20178 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
20179 $encrypted_plans[] = self::_encrypt_entity( $this->_plans[ $i ] );
20180 }
20181
20182 $plans[ $this->_slug ] = $encrypted_plans;
20183
20184 $this->set_account_option( 'plans', $plans, $store );
20185 }
20186
20187 /**
20188 * Update user's plugin licenses.
20189 *
20190 * @author Vova Feldman (@svovaf)
20191 * @since 1.0.5
20192 *
20193 * @param bool $store
20194 * @param number|bool $module_id
20195 * @param FS_Plugin_License[] $licenses
20196 */
20197 private function _store_licenses( $store = true, $module_id = false, $licenses = array() ) {
20198 $this->_logger->entrance();
20199
20200 $all_licenses = self::get_all_licenses();
20201
20202 if ( ! FS_Plugin::is_valid_id( $module_id ) ) {
20203 $module_id = $this->_module_id;
20204
20205 $user_licenses = is_array( $this->_licenses ) ?
20206 $this->_licenses :
20207 array();
20208
20209 if ( empty( $user_licenses ) ) {
20210 // If the context user doesn't have any license, don't update the licenses collection.
20211 return;
20212 }
20213
20214 $new_user_licenses_map = array();
20215 foreach ( $user_licenses as $user_license ) {
20216 $new_user_licenses_map[ $user_license->id ] = $user_license;
20217 }
20218
20219 self::store_user_id_license_ids_map( array_keys( $new_user_licenses_map ), $this->_module_id, $this->_user->id );
20220
20221 // Update user licenses.
20222 $licenses_to_update_count = count( $new_user_licenses_map );
20223 foreach ( $all_licenses[ $module_id ] as $key => $license ) {
20224 if ( 0 === $licenses_to_update_count ) {
20225 break;
20226 }
20227
20228 if ( isset( $new_user_licenses_map[ $license->id ] ) ) {
20229 // Update license.
20230 $all_licenses[ $module_id ][ $key ] = $new_user_licenses_map[ $license->id ];
20231 unset( $new_user_licenses_map[ $license->id ] );
20232
20233 $licenses_to_update_count --;
20234 }
20235 }
20236
20237 if ( ! empty( $new_user_licenses_map ) ) {
20238 // Add new licenses.
20239 $all_licenses[ $module_id ] = array_merge( array_values( $new_user_licenses_map ), $all_licenses[ $module_id ] );
20240 }
20241
20242 $licenses = $all_licenses[ $module_id ];
20243 }
20244
20245 if ( ! isset( $all_licenses[ $module_id ] ) ) {
20246 $all_licenses[ $module_id ] = array();
20247 }
20248
20249 $all_licenses[ $module_id ] = $licenses;
20250
20251 self::$_accounts->set_option( 'all_licenses', $all_licenses, $store );
20252 }
20253
20254 /**
20255 * Update user information.
20256 *
20257 * @author Vova Feldman (@svovaf)
20258 * @since 1.0.1
20259 *
20260 * @param bool $store Flush to Database if true.
20261 */
20262 private function _store_user( $store = true ) {
20263 $this->_logger->entrance();
20264
20265 if ( empty( $this->_user->id ) ) {
20266 $this->_logger->error( "Empty user ID, can't store user." );
20267
20268 return;
20269 }
20270
20271 $users = self::get_all_users();
20272 $users[ $this->_user->id ] = $this->_user;
20273 self::$_accounts->set_option( 'users', $users, $store );
20274 }
20275
20276 /**
20277 * Update new updates information.
20278 *
20279 * @author Vova Feldman (@svovaf)
20280 * @since 1.0.4
20281 *
20282 * @param FS_Plugin_Tag|null $update
20283 * @param bool $store Flush to Database if true.
20284 * @param bool|number $plugin_id
20285 */
20286 private function _store_update( $update, $store = true, $plugin_id = false ) {
20287 $this->_logger->entrance();
20288
20289 if ( $update instanceof FS_Plugin_Tag ) {
20290 $update->updated = time();
20291 }
20292
20293 if ( ! is_numeric( $plugin_id ) ) {
20294 $plugin_id = $this->_plugin->id;
20295 }
20296
20297 $updates = self::get_all_updates();
20298 $updates[ $plugin_id ] = $update;
20299 self::$_accounts->set_option( 'updates', $updates, $store );
20300 }
20301
20302 /**
20303 * Update new updates information.
20304 *
20305 * @author Vova Feldman (@svovaf)
20306 * @since 1.0.6
20307 *
20308 * @param FS_Plugin[] $plugin_addons
20309 * @param bool $store Flush to Database if true.
20310 */
20311 private function _store_addons( $plugin_addons, $store = true ) {
20312 $this->_logger->entrance();
20313
20314 $addons = self::get_all_addons();
20315 $addons[ $this->_plugin->id ] = $plugin_addons;
20316 self::$_accounts->set_option( 'addons', $addons, $store );
20317 }
20318
20319 /**
20320 * Delete plugin's associated add-ons.
20321 *
20322 * @author Vova Feldman (@svovaf)
20323 * @since 1.0.8
20324 *
20325 * @param bool $store
20326 *
20327 * @return bool
20328 */
20329 private function _delete_account_addons( $store = true ) {
20330 $all_addons = self::get_all_account_addons();
20331
20332 if ( ! isset( $all_addons[ $this->_plugin->id ] ) ) {
20333 return false;
20334 }
20335
20336 unset( $all_addons[ $this->_plugin->id ] );
20337
20338 self::$_accounts->set_option( 'account_addons', $all_addons, $store );
20339
20340 return true;
20341 }
20342
20343 /**
20344 * Update account add-ons list.
20345 *
20346 * @author Vova Feldman (@svovaf)
20347 * @since 1.0.6
20348 *
20349 * @param FS_Plugin[] $addons
20350 * @param bool $store Flush to Database if true.
20351 */
20352 private function _store_account_addons( $addons, $store = true ) {
20353 $this->_logger->entrance();
20354
20355 $all_addons = self::get_all_account_addons();
20356 $all_addons[ $this->_plugin->id ] = $addons;
20357 self::$_accounts->set_option( 'account_addons', $all_addons, $store );
20358 }
20359
20360 /**
20361 * Purges the cache for the valid user licenses API call so that when the `Account` or `Add-Ons` page is loaded,
20362 * the valid user licenses will be fetched again and the account add-ons may be updated.
20363 *
20364 * @author Leo Fajardo (@leorw)
20365 * @since 2.2.4
20366 */
20367 private function purge_valid_user_licenses_cache() {
20368 if ( ! $this->is_registered() ) {
20369 return;
20370 }
20371
20372 $this->get_api_user_scope()->purge_cache( $this->get_valid_user_licenses_endpoint() );
20373 }
20374
20375 /**
20376 * @author Leo Fajardo (@leorw)
20377 * @since 2.3.0
20378 *
20379 * @param array $all_licenses
20380 * @param number|null $site_license_id
20381 * @param bool $include_parent_licenses
20382 *
20383 * @return array
20384 */
20385 private function get_foreign_licenses_info( $all_licenses, $site_license_id = null, $include_parent_licenses = false ) {
20386 $foreign_licenses = array(
20387 'ids' => array(),
20388 'license_keys' => array()
20389 );
20390
20391 $parent_license_ids_map = array();
20392
20393 foreach ( $all_licenses as $license ) {
20394 if ( $license->user_id == $this->_user->id || $license->id == $site_license_id ) {
20395 continue;
20396 }
20397
20398 $foreign_licenses['ids'][] = $license->id;
20399 $foreign_licenses['license_keys'][] = $license->secret_key;
20400
20401 if (
20402 $include_parent_licenses &&
20403 is_object( $this->_license ) &&
20404 FS_Plugin_License::is_valid_id( $this->_license->parent_license_id ) &&
20405 ! isset( $parent_license_ids_map[ $this->_license->parent_license_id ] )
20406 ) {
20407 /**
20408 * Include the parent license's info only if it has not been included before since child licenses
20409 * can have the same parent license.
20410 */
20411 $foreign_licenses['ids'][] = $this->_license->parent_license_id;
20412 $foreign_licenses['license_keys'][] = $license->secret_key;
20413
20414 $parent_license_ids_map[ $this->_license->parent_license_id ] = true;
20415 }
20416 }
20417
20418 if ( empty( $foreign_licenses['ids'] ) ) {
20419 $foreign_licenses = array();
20420 }
20421
20422 return $foreign_licenses;
20423 }
20424
20425 /**
20426 * @author Leo Fajardo (@leorw)
20427 * @since 2.3.0
20428 *
20429 * @return string
20430 */
20431 private function get_valid_user_licenses_endpoint() {
20432 $user_licenses_endpoint = '/licenses.json?type=active' .
20433 ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? '&is_enriched=true' : '' );
20434
20435 $foreign_licenses = $this->get_foreign_licenses_info( self::get_all_licenses( $this->_module_id ), null, true );
20436
20437 if ( ! empty ( $foreign_licenses ) ) {
20438 $foreign_licenses = array(
20439 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
20440 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
20441 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
20442 );
20443
20444 $user_licenses_endpoint = add_query_arg( $foreign_licenses, $user_licenses_endpoint );
20445 }
20446
20447 return $user_licenses_endpoint;
20448 }
20449
20450 /**
20451 * Fetches active licenses that are enriched with product type if there's a context `bundle_id` and bundle
20452 * licenses enriched with product IDs if there are any. From the licenses, the `get_updated_account_addons`
20453 * method filters out non–add-on product IDs and stores the add-on IDs.
20454 *
20455 * @author Leo Fajardo (@leorw)
20456 * @since 2.2.4
20457 *
20458 * @return stdClass[] array
20459 */
20460 private function fetch_valid_user_licenses() {
20461 $this->_logger->entrance();
20462
20463 $result = $this->get_api_user_scope()->get( $this->get_valid_user_licenses_endpoint() );
20464
20465 if ( ! $this->is_api_result_object( $result, 'licenses' ) ||
20466 ! is_array( $result->licenses )
20467 ) {
20468 return array();
20469 }
20470
20471 return $result->licenses;
20472 }
20473
20474 /**
20475 * @author Leo Fajardo (@leorw)
20476 * @since 2.2.4
20477 *
20478 * @return number[] Account add-on IDs.
20479 */
20480 function get_updated_account_addons() {
20481 $addons = $this->get_addons();
20482 if ( empty( $addons ) ) {
20483 return array();
20484 }
20485
20486 $account_addons = $this->get_account_addons();
20487 if ( ! is_array( $account_addons ) ) {
20488 $account_addons = array();
20489 }
20490
20491 $user_licenses = $this->is_registered() ?
20492 $this->fetch_valid_user_licenses() :
20493 array();
20494
20495 if ( empty( $user_licenses ) ) {
20496 return $account_addons;
20497 }
20498
20499 $addon_ids = array();
20500 foreach ( $addons as $addon ) {
20501 $addon_ids[] = $addon->id;
20502 }
20503
20504 $license_product_ids = array();
20505
20506 foreach ( $user_licenses as $license ) {
20507 if ( isset( $license->plugin_type ) && 'bundle' === $license->plugin_type ) {
20508 $license_product_ids = array_merge( $license_product_ids, $license->products );
20509 } else {
20510 $license_product_ids[] = $license->plugin_id;
20511 }
20512 }
20513
20514 // Filter out non–add-on IDs.
20515 $new_account_addons = array_intersect( $addon_ids, $license_product_ids );
20516 if ( count( $new_account_addons ) !== count( $account_addons ) ) {
20517 $this->_store_account_addons( array_unique( $new_account_addons ) );
20518 }
20519
20520 return $new_account_addons;
20521 }
20522
20523 /**
20524 * Store account params in the Database.
20525 *
20526 * @author Vova Feldman (@svovaf)
20527 * @since 1.0.1
20528 *
20529 * @param null|int $blog_id Since 2.0.0
20530 */
20531 private function _store_account( $blog_id = null ) {
20532 $this->_logger->entrance();
20533
20534 $this->_store_site( false, $blog_id );
20535 $this->_store_user( false );
20536 $this->_store_plans( false );
20537 $this->_store_licenses( false );
20538
20539 self::$_accounts->store( $blog_id );
20540 }
20541
20542 /**
20543 * Sync user's information.
20544 *
20545 * @author Vova Feldman (@svovaf)
20546 * @since 1.0.3
20547 * @uses FS_Api
20548 */
20549 private function _handle_account_user_sync() {
20550 $this->_logger->entrance();
20551
20552 $api = $this->get_api_user_scope();
20553
20554 // Get user's information.
20555 $user = $api->get( '/', true );
20556
20557 if ( isset( $user->id ) ) {
20558 $this->_user->first = $user->first;
20559 $this->_user->last = $user->last;
20560 $this->_user->email = $user->email;
20561
20562 $is_menu_item_account_visible = $this->is_submenu_item_visible( 'account' );
20563
20564 if ( $user->is_verified &&
20565 ( ! isset( $this->_user->is_verified ) || false === $this->_user->is_verified )
20566 ) {
20567 $this->_user->is_verified = true;
20568
20569 $this->do_action( 'account_email_verified', $user->email );
20570
20571 $this->_admin_notices->add(
20572 $this->get_text_inline( 'Your email has been successfully verified - you are AWESOME!', 'email-verified-message' ),
20573 $this->get_text_x_inline( 'Right on', 'a positive response', 'right-on' ) . '!',
20574 'success',
20575 // Make admin sticky if account menu item is invisible,
20576 // since the page will be auto redirected to the plugin's
20577 // main settings page, and the non-sticky message
20578 // will disappear.
20579 ! $is_menu_item_account_visible,
20580 'email_verified'
20581 );
20582 }
20583
20584 // Flush user details to DB.
20585 $this->_store_user();
20586
20587 $this->do_action( 'after_account_user_sync', $user );
20588
20589 /**
20590 * If account menu item is hidden, redirect to plugin's main settings page.
20591 *
20592 * @author Vova Feldman (@svovaf)
20593 * @since 1.1.6
20594 *
20595 * @link https://github.com/Freemius/wordpress-sdk/issues/6
20596 */
20597 if ( ! $is_menu_item_account_visible ) {
20598 fs_redirect( $this->_get_admin_page_url() );
20599 }
20600 }
20601 }
20602
20603 /**
20604 * @author Vova Feldman (@svovaf)
20605 * @since 1.0.9
20606 * @uses FS_Api
20607 *
20608 * @param number|bool $license_id
20609 *
20610 * @return FS_Subscription|object|bool
20611 */
20612 private function _fetch_site_license_subscription( $license_id = false ) {
20613 $this->_logger->entrance();
20614 $api = $this->get_api_site_scope();
20615
20616 if ( ! is_numeric( $license_id ) ) {
20617 $license_id = FS_Plugin_License::is_valid_id( $this->_license->parent_license_id ) ?
20618 $this->_license->parent_license_id :
20619 $this->_license->id;
20620 }
20621
20622 $result = $api->get( "/licenses/{$license_id}/subscriptions.json", true );
20623
20624 return ! isset( $result->error ) ?
20625 ( ( is_array( $result->subscriptions ) && 0 < count( $result->subscriptions ) ) ?
20626 new FS_Subscription( $result->subscriptions[0] ) :
20627 false
20628 ) :
20629 $result;
20630 }
20631
20632 /**
20633 * @author Vova Feldman (@svovaf)
20634 * @since 1.0.4
20635 * @uses FS_Api
20636 *
20637 * @param number|bool $plan_id
20638 *
20639 * @return FS_Plugin_Plan|object
20640 */
20641 private function _fetch_site_plan( $plan_id = false ) {
20642 $this->_logger->entrance();
20643 $api = $this->get_api_site_scope();
20644
20645 if ( ! is_numeric( $plan_id ) ) {
20646 $plan_id = $this->_site->plan_id;
20647 }
20648
20649 $plan = $api->get( "/plans/{$plan_id}.json", true );
20650
20651 return ! isset( $plan->error ) ? new FS_Plugin_Plan( $plan ) : $plan;
20652 }
20653
20654 /**
20655 * @author Vova Feldman (@svovaf)
20656 * @since 1.0.5
20657 * @uses FS_Api
20658 *
20659 * @return FS_Plugin_Plan[]|object
20660 */
20661 private function _fetch_plugin_plans() {
20662 $this->_logger->entrance();
20663 $api = $this->get_current_or_network_user_api_scope();
20664
20665 /**
20666 * @since 1.2.3 When running in DEV mode, retrieve pending plans as well.
20667 */
20668 $result = $api->get( $this->add_show_pending( "/plugins/{$this->_module_id}/plans.json" ), true );
20669
20670 if ( $this->is_api_result_object( $result, 'plans' ) && is_array( $result->plans ) ) {
20671 for ( $i = 0, $len = count( $result->plans ); $i < $len; $i ++ ) {
20672 $result->plans[ $i ] = new FS_Plugin_Plan( $result->plans[ $i ] );
20673 }
20674
20675 $result = $result->plans;
20676 }
20677
20678 return $result;
20679 }
20680
20681 /**
20682 * @author Vova Feldman (@svovaf)
20683 * @since 2.0.0
20684 *
20685 * @param number $plan_id
20686 *
20687 * @return \FS_Plugin_Plan|object
20688 */
20689 private function fetch_plan_by_id( $plan_id ) {
20690 $this->_logger->entrance();
20691 $api = $this->get_current_or_network_user_api_scope();
20692
20693 $result = $api->get( "/plugins/{$this->_module_id}/plans/{$plan_id}.json", true );
20694
20695 return $this->is_api_result_entity( $result ) ?
20696 new FS_Plugin_Plan( $result ) :
20697 $result;
20698 }
20699
20700 /**
20701 * @author Vova Feldman (@svovaf)
20702 * @since 1.0.5
20703 * @uses FS_Api
20704 *
20705 * @param number|bool $plugin_id
20706 * @param number|bool $site_license_id
20707 * @param array $foreign_licenses @since 2.0.0. This is used by network-activated plugins.
20708 * @param number|null $blog_id
20709 *
20710 * @return FS_Plugin_License[]|object
20711 */
20712 private function _fetch_licenses(
20713 $plugin_id = false,
20714 $site_license_id = false,
20715 $foreign_licenses = array(),
20716 $blog_id = null
20717 ) {
20718 $this->_logger->entrance();
20719
20720 $api = $this->get_api_user_scope();
20721
20722 if ( ! is_numeric( $plugin_id ) ) {
20723 $plugin_id = $this->_plugin->id;
20724 }
20725
20726 $user_licenses_endpoint = "/plugins/{$plugin_id}/licenses.json?is_enriched=true";
20727 if ( ! empty ( $foreign_licenses ) ) {
20728 $foreign_licenses = array(
20729 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
20730 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
20731 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
20732 );
20733
20734 $user_licenses_endpoint = add_query_arg( $foreign_licenses, $user_licenses_endpoint );
20735 }
20736
20737 $result = $api->get( $user_licenses_endpoint, true );
20738
20739 $is_site_license_synced = false;
20740
20741 $api_errors = array();
20742
20743 if ( $this->is_api_result_object( $result, 'licenses' ) &&
20744 is_array( $result->licenses )
20745 ) {
20746 for ( $i = 0, $len = count( $result->licenses ); $i < $len; $i ++ ) {
20747 $result->licenses[ $i ] = new FS_Plugin_License( $result->licenses[ $i ] );
20748
20749 if ( ( ! $is_site_license_synced ) && is_numeric( $site_license_id ) ) {
20750 $is_site_license_synced = ( $site_license_id == $result->licenses[ $i ]->id );
20751 }
20752 }
20753
20754 $result = $result->licenses;
20755 } else {
20756 $api_errors[] = $result;
20757 $result = array();
20758 }
20759
20760 if ( ! $is_site_license_synced ) {
20761 if ( ! is_null( $blog_id ) ) {
20762 /**
20763 * If blog ID is not null, the request is for syncing of the license of a single site via the
20764 * network-level "Account" page.
20765 *
20766 * @author Leo Fajardo (@leorw)
20767 */
20768 $this->switch_to_blog( $blog_id );
20769 }
20770
20771 $api = $this->get_api_site_scope();
20772
20773 if ( is_numeric( $site_license_id ) ) {
20774 // Try to retrieve a foreign license that is linked to the install.
20775 $api_result = $api->call( '/licenses.json?is_enriched=true' );
20776
20777 if ( $this->is_api_result_object( $api_result, 'licenses' ) &&
20778 is_array( $api_result->licenses )
20779 ) {
20780 $licenses = $api_result->licenses;
20781
20782 if ( ! empty( $licenses ) ) {
20783 $result[] = new FS_Plugin_License( $licenses[0] );
20784 }
20785 } else {
20786 $api_errors[] = $api_result;
20787 }
20788 } else if (
20789 is_object( $this->_license ) &&
20790 /**
20791 * Sync only if the license belongs to the context plugin. `$plugin_id` can be an add-on ID while
20792 * the FS instance that does the syncing is the parent FS instance.
20793 *
20794 * @author Leo Fajardo (@leorw)
20795 * @since 2.3.0
20796 */
20797 $this->_license->plugin_id == $plugin_id
20798 ) {
20799 $is_license_in_result = false;
20800 if ( ! empty( $result ) ) {
20801 foreach ( $result as $license ) {
20802 if ( $license->id == $this->_license->id ) {
20803 $is_license_in_result = true;
20804 break;
20805 }
20806 }
20807 }
20808
20809 if ( ! $is_license_in_result ) {
20810 // Fetch foreign license by ID and license key.
20811 $license = $api->get( "/licenses/{$this->_license->id}.json?license_key=" .
20812 urlencode( $this->_license->secret_key ) . '&is_enriched=true' );
20813
20814 if ( $this->is_api_result_entity( $license ) ) {
20815 $result[] = new FS_Plugin_License( $license );
20816 } else {
20817 $api_errors[] = $license;
20818 }
20819 }
20820 }
20821
20822 if ( ! is_null( $blog_id ) ) {
20823 $this->switch_to_blog( $this->_storage->network_install_blog_id );
20824 }
20825 }
20826
20827 if ( is_array( $result ) && 0 < count( $result ) ) {
20828 // If found at least one license, return license collection even if there are errors.
20829 return $result;
20830 }
20831
20832 if ( ! empty( $api_errors ) ) {
20833 // If found any errors and no licenses, return first error.
20834 return $api_errors[0];
20835 }
20836
20837 // Fallback to empty licenses list.
20838 return $result;
20839 }
20840
20841 /**
20842 * @author Vova Feldman (@svovaf)
20843 * @since 2.0.0
20844 *
20845 * @param number $license_id
20846 * @param string $license_key
20847 *
20848 * @return \FS_Plugin_License|object
20849 */
20850 private function fetch_license_by_key( $license_id, $license_key ) {
20851 $this->_logger->entrance();
20852
20853 $api = $this->get_current_or_network_user_api_scope();
20854
20855 $result = $api->get( "/licenses/{$license_id}.json?license_key=" . urlencode( $license_key ) );
20856
20857 return $this->is_api_result_entity( $result ) ?
20858 new FS_Plugin_License( $result ) :
20859 $result;
20860 }
20861
20862 /**
20863 * @author Vova Feldman (@svovaf)
20864 * @since 1.2.0
20865 * @uses FS_Api
20866 *
20867 * @param number|bool $plugin_id
20868 * @param bool $flush
20869 *
20870 * @return FS_Payment[]|object
20871 */
20872 function _fetch_payments( $plugin_id = false, $flush = false ) {
20873 $this->_logger->entrance();
20874
20875 $api = $this->get_api_user_scope();
20876
20877 if ( ! is_numeric( $plugin_id ) ) {
20878 $plugin_id = $this->_plugin->id;
20879 }
20880
20881 $include_bundles = (
20882 is_object( $this->_plugin ) &&
20883 FS_Plugin::is_valid_id( $this->_plugin->bundle_id )
20884 );
20885
20886 $result = $api->get(
20887 "/plugins/{$plugin_id}/payments.json?include_addons=true" . ($include_bundles ? '&include_bundles=true' : ''),
20888 $flush
20889 );
20890
20891 if ( ! isset( $result->error ) ) {
20892 for ( $i = 0, $len = count( $result->payments ); $i < $len; $i ++ ) {
20893 $result->payments[ $i ] = new FS_Payment( $result->payments[ $i ] );
20894 }
20895 $result = $result->payments;
20896 }
20897
20898 return $result;
20899 }
20900
20901 /**
20902 * @author Vova Feldman (@svovaf)
20903 * @since 1.2.1.5
20904 * @uses FS_Api
20905 *
20906 * @param bool $flush
20907 *
20908 * @return \FS_Billing|mixed
20909 */
20910 function _fetch_billing( $flush = false ) {
20911 require_once WP_FS__DIR_INCLUDES . '/entities/class-fs-billing.php';
20912
20913 $billing = $this->get_api_user_scope()->get( 'billing.json', $flush );
20914
20915 if ( $this->is_api_result_entity( $billing ) ) {
20916 $billing = new FS_Billing( $billing );
20917 }
20918
20919 return $billing;
20920 }
20921
20922 /**
20923 * @author Vova Feldman (@svovaf)
20924 * @since 1.0.5
20925 *
20926 * @param FS_Plugin_License[] $licenses
20927 * @param number $module_id
20928 */
20929 private function _update_licenses( $licenses, $module_id ) {
20930 $this->_logger->entrance();
20931
20932 if ( is_array( $licenses ) ) {
20933 for ( $i = 0, $len = count( $licenses ); $i < $len; $i ++ ) {
20934 $licenses[ $i ]->updated = time();
20935 }
20936 }
20937
20938 $this->_store_licenses( true, $module_id, $licenses );
20939 }
20940
20941 /**
20942 * @author Vova Feldman (@svovaf)
20943 * @since 1.0.4
20944 *
20945 * @param bool|number $plugin_id
20946 * @param bool $flush Since 1.1.7.3
20947 * @param int $expiration Since 1.2.2.7
20948 * @param bool|string $newer_than Since 2.2.1
20949 *
20950 * @return object|false New plugin tag info if exist.
20951 */
20952 private function _fetch_newer_version( $plugin_id = false, $flush = true, $expiration = WP_FS__TIME_24_HOURS_IN_SEC, $newer_than = false ) {
20953 $latest_tag = $this->_fetch_latest_version( $plugin_id, $flush, $expiration, $newer_than );
20954
20955 if ( ! is_object( $latest_tag ) ) {
20956 return false;
20957 }
20958
20959 $plugin_version = $this->get_plugin_version();
20960
20961 // Check if version is actually newer.
20962 $has_new_version =
20963 // If it's an non-installed add-on then always return latest.
20964 ( $this->_is_addon_id( $plugin_id ) && ! $this->is_addon_activated( $plugin_id ) ) ||
20965 // Compare versions.
20966 version_compare( $plugin_version, $latest_tag->version, '<' );
20967
20968 $this->_logger->departure( $has_new_version ? 'Found newer plugin version ' . $latest_tag->version : 'No new version' );
20969
20970 $is_latest_version_beta = ( 'beta' === $latest_tag->release_mode );
20971
20972 $this->_storage->beta_data = array(
20973 'is_beta' => $is_latest_version_beta,
20974 'version' => $latest_tag->version
20975 );
20976
20977 return $has_new_version ? $latest_tag : false;
20978 }
20979
20980 /**
20981 * @author Vova Feldman (@svovaf)
20982 * @since 1.0.5
20983 *
20984 * @param bool|number $plugin_id
20985 * @param bool $flush Since 1.1.7.3
20986 * @param int $expiration Since 1.2.2.7
20987 * @param bool|string $newer_than Since 2.2.1
20988 *
20989 * @return bool|FS_Plugin_Tag
20990 */
20991 function get_update( $plugin_id = false, $flush = true, $expiration = WP_FS__TIME_24_HOURS_IN_SEC, $newer_than = false ) {
20992 $this->_logger->entrance();
20993
20994 if ( ! is_numeric( $plugin_id ) ) {
20995 $plugin_id = $this->_plugin->id;
20996 }
20997
20998 $this->check_updates( true, $plugin_id, $flush, $expiration, $newer_than );
20999 $updates = $this->get_all_updates();
21000
21001 return isset( $updates[ $plugin_id ] ) && is_object( $updates[ $plugin_id ] ) ? $updates[ $plugin_id ] : false;
21002 }
21003
21004 /**
21005 * Check if site assigned with active license.
21006 *
21007 * @author Vova Feldman (@svovaf)
21008 * @since 1.0.6
21009 *
21010 * @deprecated Please use has_active_valid_license() instead because license can be cancelled.
21011 */
21012 function has_active_license() {
21013 return (
21014 is_object( $this->_license ) &&
21015 is_numeric( $this->_license->id ) &&
21016 ! $this->_license->is_expired()
21017 );
21018 }
21019
21020 /**
21021 * Check if site assigned with active & valid (not expired) license.
21022 *
21023 * @author Vova Feldman (@svovaf)
21024 * @since 1.2.1
21025 *
21026 * @param bool $check_expiration
21027 */
21028 function has_active_valid_license( $check_expiration = true ) {
21029 return self::is_active_valid_license( $this->_license, $check_expiration );
21030 }
21031
21032 /**
21033 * @author Leo Fajardo (@leorw)
21034 * @since 2.3.1
21035 */
21036 function is_data_debug_mode() {
21037 if ( is_null( $this->is_whitelabeled ) || ! $this->is_whitelabeled ) {
21038 return false;
21039 }
21040
21041 $fs = $this->is_addon() ?
21042 $this->get_parent_instance() :
21043 $this;
21044
21045 if ( $fs->is_network_active() && fs_is_network_admin() ) {
21046 $is_developer_license_debug_mode = get_site_transient( "fs_{$this->get_id()}_data_debug_mode" );
21047 } else {
21048 $is_developer_license_debug_mode = get_transient( "fs_{$this->get_id()}_data_debug_mode" );
21049 }
21050
21051 return ( 'true' === $is_developer_license_debug_mode );
21052 }
21053
21054 /**
21055 * @author Leo Fajardo (@leorw)
21056 * @since 2.3.1
21057 */
21058 function _set_data_debug_mode() {
21059 if ( ! $this->is_whitelabeled( true ) ) {
21060 return;
21061 }
21062
21063 $license_or_user_key = fs_request_get( 'license_or_user_key' );
21064
21065 $transient_value = ( ! empty( $license_or_user_key ) ) ?
21066 'true' :
21067 'false';
21068
21069 if ( 'true' === $transient_value ) {
21070 $stored_key = $this->_storage->get( ! FS_User::is_valid_id( $this->_storage->last_license_user_id ) ?
21071 'last_license_key' :
21072 'last_license_user_key'
21073 );
21074
21075 if ( md5( $license_or_user_key ) !== $stored_key ) {
21076 $this->shoot_ajax_failure( sprintf(
21077 '%s... %s',
21078 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ),
21079 $this->get_text_inline(
21080 'seems like the key you entered doesn\'t match our records.',
21081 'developer-or-license-not-found'
21082 )
21083 ) );
21084 }
21085 }
21086
21087 if ( $this->is_network_active() && fs_is_network_admin() ) {
21088 set_site_transient(
21089 "fs_{$this->get_id()}_data_debug_mode",
21090 $transient_value,
21091 WP_FS__TIME_24_HOURS_IN_SEC / 24
21092 );
21093 } else {
21094 set_transient(
21095 "fs_{$this->get_id()}_data_debug_mode",
21096 $transient_value,
21097 WP_FS__TIME_24_HOURS_IN_SEC / 24
21098 );
21099 }
21100
21101 if ( 'true' === $transient_value ) {
21102 $this->_admin_notices->add_sticky(
21103 $this->get_text_inline(
21104 '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.',
21105 'data_debug_mode_enabled'
21106 ),
21107 'data_debug_mode_enabled'
21108 );
21109 }
21110
21111 $this->shoot_ajax_success();
21112 }
21113
21114 /**
21115 * Check if a given license is active & valid (not expired).
21116 *
21117 * @author Vova Feldman (@svovaf)
21118 * @since 2.1.3
21119 *
21120 * @param FS_Plugin_License $license
21121 * @param bool $check_expiration
21122 *
21123 * @return bool
21124 */
21125 private static function is_active_valid_license( $license, $check_expiration = true ) {
21126 return (
21127 is_object( $license ) &&
21128 FS_Plugin_License::is_valid_id( $license->id ) &&
21129 $license->is_active() &&
21130 ( ! $check_expiration || $license->is_valid() )
21131 );
21132 }
21133
21134 /**
21135 * Checks if there's any site that is associated with an active & valid license.
21136 * This logic is used to determine if the admin can download the premium code base from a network level admin.
21137 *
21138 * @author Vova Feldman (@svovaf)
21139 * @since 2.1.3
21140 *
21141 * @return bool
21142 */
21143 function has_any_active_valid_license() {
21144 if ( ! fs_is_network_admin() ) {
21145 return $this->has_active_valid_license();
21146 }
21147
21148 $installs = $this->get_blog_install_map();
21149 $all_plugin_licenses = self::get_all_licenses( $this->_module_id );
21150
21151 foreach ( $installs as $blog_id => $install ) {
21152 if ( ! FS_Plugin_License::is_valid_id( $install->license_id ) ) {
21153 continue;
21154 }
21155
21156 foreach ( $all_plugin_licenses as $license ) {
21157 if ( $license->id == $install->license_id ) {
21158 if ( self::is_active_valid_license( $license ) ) {
21159 return true;
21160 }
21161 }
21162 }
21163 }
21164
21165 return false;
21166 }
21167
21168 /**
21169 * Check if site assigned with license with enabled features.
21170 *
21171 * @author Vova Feldman (@svovaf)
21172 * @since 1.0.6
21173 *
21174 * @return bool
21175 */
21176 function has_features_enabled_license() {
21177 return (
21178 is_object( $this->_license ) &&
21179 is_numeric( $this->_license->id ) &&
21180 $this->_license->is_features_enabled()
21181 );
21182 }
21183
21184 /**
21185 * Checks if the product is activated with a bundle license.
21186 *
21187 * @author Leo Fajardo (@leorw)
21188 * @since 2.4.0
21189 *
21190 * @return bool
21191 */
21192 function is_activated_with_bundle_license() {
21193 if ( ! $this->has_features_enabled_license() ) {
21194 return false;
21195 }
21196
21197 return FS_Plugin_License::is_valid_id( $this->_license->parent_license_id );
21198 }
21199
21200 /**
21201 * Check if user is a trial or have feature enabled license.
21202 *
21203 * @author Vova Feldman (@svovaf)
21204 * @since 1.1.7
21205 *
21206 * @return bool
21207 */
21208 function can_use_premium_code() {
21209 return $this->is_trial() || $this->has_features_enabled_license();
21210 }
21211
21212 /**
21213 * Checks if the current user can activate plugins or switch themes. Note that this method should only be used
21214 * after the `init` action is triggered because it is using `current_user_can()` which is only functional after
21215 * the context user is authenticated.
21216 *
21217 * @author Leo Fajardo (@leorw)
21218 * @since 1.2.2
21219 *
21220 * @return bool
21221 */
21222 function is_user_admin() {
21223 /**
21224 * Require a super-admin when network activated, running from the network level OR if
21225 * running from the site level but not delegated the opt-in.
21226 *
21227 * @author Vova Feldman (@svovaf)
21228 * @since 2.0.0
21229 */
21230 if ( $this->_is_network_active &&
21231 ( fs_is_network_admin() || ! $this->is_delegated_connection() )
21232 ) {
21233 return is_super_admin();
21234 }
21235
21236 return ( $this->is_plugin() && current_user_can( is_multisite() ? 'manage_options' : 'activate_plugins' ) )
21237 || ( $this->is_theme() && current_user_can( 'switch_themes' ) );
21238 }
21239
21240 /**
21241 * Sync site's plan.
21242 *
21243 * @author Vova Feldman (@svovaf)
21244 * @since 1.0.3
21245 *
21246 * @uses FS_Api
21247 *
21248 * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by
21249 * the admin.
21250 * @param bool $is_context_single_site @since 2.0.0. This is used when syncing a license for a single install from the
21251 * network-level "Account" page.
21252 * @param int|null $current_blog_id @since 2.2.3. This is passed from the `execute_cron` method and used by the
21253 * `_sync_plugin_license` method in order to switch to the previous blog when sending
21254 * updates for a single site in case `execute_cron` has switched to a different blog.
21255 */
21256 private function _sync_license( $background = false, $is_context_single_site = false, $current_blog_id = null ) {
21257 $this->_logger->entrance();
21258
21259 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
21260
21261 $is_addon_sync = ( ! $this->_plugin->is_addon() && $plugin_id != $this->get_id() );
21262
21263 if ( $is_addon_sync ) {
21264 $this->_sync_addon_license( $plugin_id, $background );
21265 } else {
21266 $this->_sync_plugin_license( $background, true, $is_context_single_site, $current_blog_id );
21267 }
21268
21269 $this->do_action( 'after_account_plan_sync', $this->get_plan_name() );
21270 }
21271
21272 /**
21273 * Sync plugin's add-on license.
21274 *
21275 * @author Vova Feldman (@svovaf)
21276 * @since 1.0.6
21277 * @uses FS_Api
21278 *
21279 * @param number $addon_id
21280 * @param bool $background
21281 */
21282 private function _sync_addon_license( $addon_id, $background ) {
21283 $this->_logger->entrance();
21284
21285 if ( $this->is_addon_activated( $addon_id ) ) {
21286 // If already installed, use add-on sync.
21287 $fs_addon = self::get_instance_by_id( $addon_id );
21288
21289 if (
21290 // Add-on is network activated and network integrated.
21291 $fs_addon->is_network_active() ||
21292 // Background sync cron.
21293 self::is_cron() ||
21294 // Add-on is not network activated or not network integrated.
21295 ! fs_is_network_admin()
21296 ) {
21297 $fs_addon->_sync_license( $background );
21298
21299 return;
21300 }
21301 }
21302
21303 // Validate add-on exists.
21304 $addon = $this->get_addon( $addon_id );
21305
21306 if ( ! is_object( $addon ) ) {
21307 return;
21308 }
21309
21310 // Add add-on into account add-ons.
21311 $account_addons = $this->get_account_addons();
21312 if ( ! is_array( $account_addons ) ) {
21313 $account_addons = array();
21314 }
21315 $account_addons[] = $addon->id;
21316 $account_addons = array_unique( $account_addons );
21317 $this->_store_account_addons( $account_addons );
21318
21319 // Load add-on licenses.
21320 $licenses = $this->_fetch_licenses( $addon->id );
21321
21322 // Sync add-on licenses.
21323 if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) {
21324 $this->_update_licenses( $licenses, $addon->id );
21325
21326 if ( ! $this->is_addon_installed( $addon->id ) && FS_License_Manager::has_premium_license( $licenses ) ) {
21327 $plans_result = $this->get_api_site_or_plugin_scope()->get( $this->add_show_pending( "/addons/{$addon_id}/plans.json" ) );
21328
21329 if ( ! isset( $plans_result->error ) ) {
21330 $plans = array();
21331 foreach ( $plans_result->plans as $plan ) {
21332 $plans[] = new FS_Plugin_Plan( $plan );
21333 }
21334
21335 $this->_admin_notices->add_sticky(
21336 sprintf(
21337 ( FS_Plan_Manager::instance()->has_free_plan( $plans ) ?
21338 $this->get_text_inline( 'Your %s Add-on plan was successfully upgraded.', 'addon-successfully-upgraded-message' ) :
21339 /* translators: %s:product name, e.g. Facebook add-on was successfully... */
21340 $this->get_text_inline( '%s Add-on was successfully purchased.', 'addon-successfully-purchased-message' ) ),
21341 $addon->title
21342 ) . ' ' . $this->get_latest_download_link(
21343 $this->get_text_inline( 'Download the latest version', 'download-latest-version' ),
21344 $addon_id
21345 ),
21346 'addon_plan_upgraded_' . $addon->slug,
21347 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
21348 );
21349 }
21350 }
21351 }
21352 }
21353
21354 /**
21355 * Sync site's plugin plan.
21356 *
21357 * @author Vova Feldman (@svovaf)
21358 * @since 1.0.6
21359 * @uses FS_Api
21360 *
21361 * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by the admin.
21362 * @param bool $send_installs_update Since 2.0.0
21363 * @param bool $is_context_single_site Since 2.0.0. This is used when sending an update for a single install and
21364 * syncing its license from the network-level "Account" page (e.g.: after
21365 * activating a license only for the single install).
21366 * @param int|null $current_blog_id Since 2.2.3. This is passed from the `execute_cron` method so that it
21367 * can be used here to switch to the previous blog in case `execute_cron`
21368 * has switched to a different blog.
21369 */
21370 private function _sync_plugin_license(
21371 $background = false,
21372 $send_installs_update = true,
21373 $is_context_single_site = false,
21374 $current_blog_id = null
21375 ) {
21376 $this->_logger->entrance();
21377
21378 $plan_change = 'none';
21379
21380 $is_site_level_sync = ( $is_context_single_site || fs_is_blog_admin() || ! $this->_is_network_active );
21381
21382 if ( ! $send_installs_update ) {
21383 $site = $this->_site;
21384 } else {
21385 /**
21386 * Sync site info.
21387 *
21388 * @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.
21389 */
21390 if ( $is_site_level_sync ) {
21391 /**
21392 * Switch to the previous blog since `execute_cron` may have switched to a different blog.
21393 *
21394 * @author Leo Fajardo (@leorw)
21395 * @since 2.2.3
21396 */
21397 if ( is_numeric( $current_blog_id ) ) {
21398 $this->switch_to_blog( $current_blog_id );
21399 }
21400
21401 $result = $this->send_install_update( array(), true, true );
21402 $is_valid = $this->is_api_result_entity( $result );
21403 } else {
21404 $result = $this->send_installs_update( array(), true, true );
21405 $is_valid = $this->is_api_result_object( $result, 'installs' );
21406 }
21407
21408 if ( ! $is_valid ) {
21409 if ( $is_context_single_site ) {
21410 // Switch back to the main blog so that the following logic will have the right entities.
21411 $this->switch_to_blog( $this->_storage->network_install_blog_id );
21412 }
21413
21414 // Show API message only if not background sync or if paying customer.
21415 if ( ! $background || $this->is_paying() ) {
21416 // Try to ping API to see if not blocked.
21417 if ( ! FS_Api::test() ) {
21418 /**
21419 * Failed to ping API - blocked!
21420 *
21421 * @author Vova Feldman (@svovaf)
21422 * @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.
21423 */
21424
21425 if ( ! self::$_global_admin_notices->has_sticky( 'api_blocked' ) ) {
21426 // Add notice immediately if not a background sync.
21427 $add_notice = ( ! $background );
21428
21429 if ( ! $add_notice ) {
21430 $counter = (int) get_transient( '_fs_api_connection_retry_counter' );
21431
21432 // We only want to add the notice after 3 consecutive failures.
21433 $add_notice = ( 3 <= $counter );
21434
21435 if ( ! $add_notice ) {
21436 /**
21437 * 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.
21438 *
21439 * 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.
21440 */
21441 set_transient( '_fs_api_connection_retry_counter', $counter + 1, WP_FS__TIME_WEEK_IN_SEC );
21442 }
21443 }
21444
21445 // Add notice instantly for not-background sync and only after 3 failed attempts for background sync.
21446 if ( $add_notice ) {
21447 self::$_global_admin_notices->add(
21448 sprintf(
21449 $this->get_text_inline( 'Your server is blocking the access to Freemius\' API, which is crucial for %1$s synchronization. Please contact your host to whitelist %2$s', 'server-blocking-access' ),
21450 $this->get_plugin_name(),
21451 '<b>' . implode( ', ', $this->apply_filters( 'api_domains', array(
21452 'api.freemius.com',
21453 'wp.freemius.com'
21454 ) ) ) . '</b>'
21455 ) . '<br> ' . $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . var_export( $result->error, true ),
21456 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
21457 'error',
21458 $background,
21459 'api_blocked'
21460 );
21461
21462 // Notice was just shown, reset connectivity counter.
21463 delete_transient( '_fs_api_connection_retry_counter' );
21464 }
21465 }
21466 } else if ( is_object( $result ) ) {
21467 // Authentication params are broken.
21468 $this->_admin_notices->add(
21469 $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 ),
21470 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
21471 'error'
21472 );
21473 }
21474 }
21475
21476 // No reason to continue with license sync while there are API issues.
21477 return;
21478 }
21479
21480 // API is working now. Delete the transient and start afresh.
21481 delete_transient('_fs_api_connection_retry_counter');
21482
21483 if ( $is_site_level_sync ) {
21484 $site = new FS_Site( $result );
21485 } else {
21486 // Map site addresses to their blog IDs.
21487 $address_to_blog_map = $this->get_address_to_blog_map();
21488
21489 // Find the current context install.
21490 $site = null;
21491 foreach ( $result->installs as $install ) {
21492 if ( $install->id == $this->_site->id ) {
21493 $site = new FS_Site( $install );
21494 } else {
21495 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
21496 $blog_id = $address_to_blog_map[ $address ];
21497
21498 $this->_store_site( true, $blog_id, new FS_Site( $install ) );
21499 }
21500 }
21501 }
21502
21503 // Sync plans.
21504 $this->_sync_plans();
21505 }
21506
21507 // Remove sticky API connectivity message.
21508 self::$_global_admin_notices->remove_sticky( 'api_blocked' );
21509
21510 if ( ! $this->has_paid_plan() ) {
21511 $this->_site = $site;
21512 $this->_store_site(
21513 true,
21514 $is_site_level_sync ?
21515 null :
21516 $this->get_network_install_blog_id()
21517 );
21518 } else {
21519 $context_blog_id = 0;
21520
21521 if ( $is_context_single_site ) {
21522 $context_blog_id = get_current_blog_id();
21523
21524 // Switch back to the main blog in order to properly sync the license.
21525 $this->switch_to_blog( $this->_storage->network_install_blog_id );
21526 }
21527
21528 /**
21529 * Sync licenses. Pass the site's license ID so that the foreign licenses will be fetched if the license
21530 * associated with that ID is not included in the user's licenses collection.
21531 */
21532 $this->_sync_licenses(
21533 $site->license_id,
21534 ( $is_context_single_site ?
21535 $context_blog_id :
21536 null
21537 )
21538 );
21539
21540 if ( $is_context_single_site ) {
21541 $this->switch_to_blog( $context_blog_id );
21542 }
21543
21544 // Check if plan / license changed.
21545 if ( $site->plan_id != $this->_site->plan_id ||
21546 // Check if trial started.
21547 $site->trial_plan_id != $this->_site->trial_plan_id ||
21548 $site->trial_ends != $this->_site->trial_ends ||
21549 // Check if license changed.
21550 $site->license_id != $this->_site->license_id
21551 ) {
21552 if ( $site->is_trial() && ( ! $this->_site->is_trial() || $site->trial_ends != $this->_site->trial_ends ) ) {
21553 // New trial started.
21554 $this->_site = $site;
21555 $plan_change = 'trial_started';
21556
21557 // For trial with subscription use-case.
21558 $new_license = is_null( $site->license_id ) ? null : $this->_get_license_by_id( $site->license_id );
21559
21560 if ( is_object( $new_license ) && $new_license->is_valid() ) {
21561 $this->_site = $site;
21562 $this->_update_site_license( $new_license );
21563 $this->_store_licenses();
21564
21565 $this->_sync_site_subscription( $this->_license );
21566 }
21567 } else if ( $this->_site->is_trial() && ! $site->is_trial() && ! is_numeric( $site->license_id ) ) {
21568 // Was in trial, but now trial expired and no license ID.
21569 // New trial started.
21570 $this->_site = $site;
21571 $plan_change = 'trial_expired';
21572 } else {
21573 $is_free = $this->is_free_plan();
21574
21575 // Make sure license exist and not expired.
21576 $new_license = is_null( $site->license_id ) ?
21577 null :
21578 $this->_get_license_by_id( $site->license_id );
21579
21580 if ( $is_free && is_null( $new_license ) && $this->has_any_license() && $this->_license->is_cancelled ) {
21581 // License cancelled.
21582 $this->_site = $site;
21583 $this->_update_site_license( $new_license );
21584 $this->_store_licenses();
21585
21586 $plan_change = 'cancelled';
21587 } else if ( $is_free && ( ( ! is_object( $new_license ) || $new_license->is_expired() ) ) ) {
21588 // The license is expired, so ignore upgrade method.
21589 $this->_site = $site;
21590 } else {
21591 // License changed.
21592 $this->_site = $site;
21593
21594 /**
21595 * IMPORTANT:
21596 * 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.
21597 *
21598 * @author Vova Feldman (@svovaf)
21599 * @since 2.0.0
21600 */
21601 $this->_update_site_license( $new_license );
21602
21603 if ( ! $is_context_single_site &&
21604 fs_is_network_admin() &&
21605 $this->_is_network_active &&
21606 $new_license->quota > 1 &&
21607 get_blog_count() > 1
21608 ) {
21609 // See if license can activated on all sites.
21610 if ( ! $this->try_activate_license_on_network( $this->_user, $new_license ) ) {
21611 if ( ! fs_request_get_bool( 'auto_install' ) ) {
21612 // Open the license activation dialog box on the account page.
21613 add_action( 'admin_footer', array(
21614 &$this,
21615 '_open_license_activation_dialog_box'
21616 ) );
21617 }
21618 }
21619 }
21620
21621 $this->_store_licenses();
21622
21623 $plan_change = $is_free ?
21624 ( $this->is_only_premium() ? 'activated' : 'upgraded' ) :
21625 ( is_object( $new_license ) ?
21626 'changed' :
21627 'downgraded' );
21628 }
21629 }
21630
21631 // Store updated site info.
21632 $this->_store_site(
21633 true,
21634 $is_site_level_sync ?
21635 null :
21636 $this->get_network_install_blog_id()
21637 );
21638 } else {
21639 if ( ! is_object( $this->_license ) ) {
21640 $this->maybe_update_whitelabel_flag(
21641 FS_Plugin_License::is_valid_id( $site->license_id ) ?
21642 $this->get_license_by_id( $site->license_id ) :
21643 null
21644 );
21645 } else {
21646 $this->maybe_update_whitelabel_flag( $this->_license );
21647
21648 if ( $this->_license->is_expired() ) {
21649 if ( ! $this->has_features_enabled_license() ) {
21650 $this->_deactivate_license();
21651 $plan_change = 'downgraded';
21652 } else {
21653 $last_time_expired_license_notice_was_shown = $this->_storage->get( 'expired_license_notice_shown', 0 );
21654
21655 if ( time() - ( 14 * WP_FS__TIME_24_HOURS_IN_SEC ) >= $last_time_expired_license_notice_was_shown ) {
21656 /**
21657 * Show the expired license notice every 14 days.
21658 *
21659 * @author Leo Fajardo (@leorw)
21660 * @since 2.3.1
21661 */
21662 $plan_change = 'expired';
21663 }
21664 }
21665 }
21666 }
21667
21668 if ( is_numeric( $site->license_id ) && is_object( $this->_license ) ) {
21669 $this->_sync_site_subscription( $this->_license );
21670 }
21671 }
21672
21673 if ( ! $this->is_addon() &&
21674 $this->_site->is_beta() !== $site->is_beta()
21675 ) {
21676 // Beta flag updated.
21677 $this->_site = $site;
21678
21679 $this->_store_site(
21680 true,
21681 $is_site_level_sync ?
21682 null :
21683 $this->get_network_install_blog_id()
21684 );
21685 }
21686
21687 if ( $this->is_addon() || $this->has_addons() ) {
21688 /**
21689 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
21690 * an updated valid user licenses collection will be fetched from the server which is used to also
21691 * update the account add-ons (add-ons the user has licenses for).
21692 *
21693 * @author Leo Fajardo (@leorw)
21694 * @since 2.2.4
21695 */
21696 $this->purge_valid_user_licenses_cache();
21697 }
21698 }
21699
21700 $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...';
21701
21702 if ( $this->apply_filters( 'has_paid_plan_account', $this->has_paid_plan() ) ) {
21703 switch ( $plan_change ) {
21704 case 'none':
21705 if ( ! $background && is_admin() ) {
21706 $plan = $this->is_trial() ?
21707 $this->get_trial_plan() :
21708 $this->get_plan();
21709
21710 if ( $plan->is_free() ) {
21711 $this->_admin_notices->add(
21712 sprintf(
21713 $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' ),
21714 '<i><b>' . $plan->title . ( $this->is_trial() ? ' ' . $this->get_text_x_inline( 'Trial', 'trial period', 'trial' ) : '' ) . '</b></i>'
21715 ) . ' ' . sprintf(
21716 '<a href="%s">%s</a>',
21717 $this->contact_url(
21718 'bug',
21719 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' ),
21720 strtoupper( $plan->name )
21721 )
21722 ),
21723 $this->get_text_inline( 'Please contact us here', 'contact-us-here' )
21724 ),
21725 $hmm_text
21726 );
21727 }
21728 }
21729 break;
21730 case 'upgraded':
21731 case 'activated':
21732 $this->_admin_notices->add_sticky(
21733 ( 'activated' === $plan_change ) ?
21734 $this->get_text_inline( 'Your plan was successfully activated.', 'plan-activated-message' ) :
21735 $this->get_text_inline( 'Your plan was successfully upgraded.', 'plan-upgraded-message' ) .
21736 $this->get_complete_upgrade_instructions(),
21737 'plan_upgraded',
21738 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
21739 );
21740
21741 $this->_admin_notices->remove_sticky( array(
21742 'trial_started',
21743 'trial_promotion',
21744 'trial_expired',
21745 'activation_complete',
21746 'license_expired',
21747 ) );
21748 break;
21749 case 'changed':
21750 $this->_admin_notices->add_sticky(
21751 sprintf(
21752 $this->get_text_inline( 'Your plan was successfully changed to %s.', 'plan-changed-to-x-message' ),
21753 $this->get_plan_title()
21754 ),
21755 'plan_changed'
21756 );
21757
21758 $this->_admin_notices->remove_sticky( array(
21759 'trial_started',
21760 'trial_promotion',
21761 'trial_expired',
21762 'activation_complete',
21763 ) );
21764 break;
21765 case 'downgraded':
21766 $this->_admin_notices->add_sticky(
21767 ($this->has_free_plan() ?
21768 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 ) :
21769 /* translators: %1$s: product title; %2$s, %3$s: wrapping HTML anchor element; %4$s: 'plugin', 'theme', or 'add-on'. */
21770 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) ) ),
21771 'license_expired',
21772 $hmm_text
21773 );
21774 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21775 break;
21776 case 'cancelled':
21777 $this->_admin_notices->add(
21778 $this->get_text_inline( 'Your license has been cancelled. If you think it\'s a mistake, please contact support.', 'license-cancelled' ) . ' ' .
21779 sprintf(
21780 '<a href="%s">%s</a>',
21781 $this->contact_url( 'bug' ),
21782 $this->get_text_inline( 'Please contact us here', 'contact-us-here' )
21783 ),
21784 $hmm_text,
21785 'error'
21786 );
21787 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21788 break;
21789 case 'expired':
21790 $this->_admin_notices->add_sticky(
21791 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 ),
21792 'license_expired',
21793 $hmm_text
21794 );
21795
21796 $this->_storage->expired_license_notice_shown = WP_FS__SCRIPT_START_TIME;
21797
21798 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
21799 break;
21800 case 'trial_started':
21801 $this->_admin_notices->add_sticky(
21802 sprintf(
21803 $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ),
21804 '<i>' . $this->get_plugin_name() . '</i>'
21805 ) . $this->get_complete_upgrade_instructions( $this->get_trial_plan()->title ),
21806 'trial_started',
21807 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
21808 );
21809
21810 $this->_admin_notices->remove_sticky( array(
21811 'trial_promotion',
21812 ) );
21813 break;
21814 case 'trial_expired':
21815 $this->_admin_notices->add_sticky(
21816 ($this->has_free_plan() ?
21817 $this->get_text_inline( 'Your free trial has expired. You can still continue using all our free features.', 'trial-expired-message' ) :
21818 /* translators: %1$s: product title; %2$s, %3$s: wrapping HTML anchor element; %4$s: 'plugin', 'theme', or 'add-on'. */
21819 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))),
21820 'trial_expired',
21821 $hmm_text
21822 );
21823 $this->_admin_notices->remove_sticky( array(
21824 'trial_started',
21825 'trial_promotion',
21826 'plan_upgraded',
21827 ) );
21828 break;
21829 }
21830 }
21831
21832 if ( 'none' !== $plan_change ) {
21833 if (
21834 ! is_object( $this->_license ) ||
21835 ! $this->_license->is_whitelabeled
21836 ) {
21837 $this->_admin_notices->remove_sticky( 'license_whitelabeled' );
21838 }
21839
21840 $this->do_action( 'after_license_change', $plan_change, $this->get_plan() );
21841 }
21842 }
21843
21844 /**
21845 * Include the required JS at the footer of the admin to trigger the license activation dialog box.
21846 *
21847 * @author Vova Feldman (@svovaf)
21848 * @since 2.0.0
21849 */
21850 public function _open_license_activation_dialog_box() {
21851 $vars = array( 'license_id' => $this->_site->license_id );
21852 fs_require_once_template( 'js/open-license-activation.php', $vars );
21853 }
21854
21855 /**
21856 * @author Vova Feldman (@svovaf)
21857 * @since 1.0.5
21858 *
21859 * @param bool $background
21860 * @param FS_Plugin_License|null $premium_license
21861 */
21862 protected function _activate_license( $background = false, $premium_license = null ) {
21863 $this->_logger->entrance();
21864
21865 if ( is_null( $premium_license ) ) {
21866 $license_id = fs_request_get( 'license_id' );
21867
21868 if ( is_object( $this->_site ) &&
21869 FS_Plugin_License::is_valid_id( $license_id ) &&
21870 $license_id == $this->_site->license_id
21871 ) {
21872 // License is already activated.
21873 return;
21874 }
21875
21876 $premium_license = FS_Plugin_License::is_valid_id( $license_id ) ?
21877 $this->_get_license_by_id( $license_id ) :
21878 $this->_get_available_premium_license();
21879 }
21880
21881 if ( ! is_object( $premium_license ) ) {
21882 return;
21883 }
21884
21885 if ( ! is_object( $this->_site ) ) {
21886 // Not yet opted-in.
21887 $user = $this->get_current_or_network_user();
21888 if ( ! is_object( $user ) ) {
21889 $user = self::_get_user_by_id( $premium_license->user_id );
21890 }
21891
21892 if ( is_object( $user ) ) {
21893 $this->install_with_user( $user, $premium_license->secret_key, false, false, false );
21894 } else {
21895 $this->opt_in(
21896 false,
21897 false,
21898 false,
21899 $premium_license->secret_key
21900 );
21901
21902 return;
21903 }
21904 }
21905
21906
21907 /**
21908 * If the premium license is already associated with the install, just
21909 * update the license reference (activation is not required).
21910 *
21911 * @since 1.1.9
21912 */
21913 if ( $premium_license->id == $this->_site->license_id ) {
21914 // License is already activated.
21915 $this->_update_site_license( $premium_license );
21916 $this->_store_account();
21917
21918 return;
21919 }
21920
21921 if ( $this->_site->user_id != $premium_license->user_id ) {
21922 $api_request_params = array( 'license_key' => $premium_license->secret_key );
21923 } else {
21924 $api_request_params = array();
21925 }
21926
21927 $api = $this->get_api_site_scope();
21928 $license = $api->call( "/licenses/{$premium_license->id}.json?is_enriched=true", 'put', $api_request_params );
21929
21930 if ( ! $this->is_api_result_entity( $license ) ) {
21931 if ( ! $background ) {
21932 $this->_admin_notices->add( sprintf(
21933 '%s %s',
21934 $this->get_text_inline( 'It looks like the license could not be activated.', 'license-activation-failed-message' ),
21935 ( is_object( $license ) && isset( $license->error ) ?
21936 $license->error->message :
21937 sprintf( '%s<br><code>%s</code>',
21938 $this->get_text_inline( 'Error received from the server:', 'server-error-message' ),
21939 var_export( $license, true )
21940 )
21941 )
21942 ),
21943 $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...',
21944 'error'
21945 );
21946 }
21947
21948 return;
21949 }
21950
21951 $premium_license = new FS_Plugin_License( $license );
21952
21953 // Updated site plan.
21954 $site = $this->get_api_site_scope()->get( '/', true );
21955 if ( $this->is_api_result_entity( $site ) ) {
21956 $this->_site = new FS_Site( $site );
21957 }
21958 $this->_update_site_license( $premium_license );
21959
21960 $this->_store_account();
21961
21962 if ( $this->is_addon() || $this->has_addons() ) {
21963 /**
21964 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
21965 * an updated valid user licenses collection will be fetched from the server which is used to also
21966 * update the account add-ons (add-ons the user has licenses for).
21967 *
21968 * @author Leo Fajardo (@leorw)
21969 * @since 2.2.4
21970 */
21971 $this->purge_valid_user_licenses_cache();
21972 }
21973
21974 if ( ! $background ) {
21975 $this->_admin_notices->add_sticky(
21976 $this->get_text_inline( 'Your license was successfully activated.', 'license-activated-message' ) .
21977 $this->get_complete_upgrade_instructions(),
21978 'license_activated',
21979 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
21980 );
21981 }
21982
21983 $this->_admin_notices->remove_sticky( array(
21984 'trial_promotion',
21985 'license_expired',
21986 ) );
21987 }
21988
21989 /**
21990 * @author Vova Feldman (@svovaf)
21991 * @since 1.0.5
21992 *
21993 * @param bool $show_notice
21994 */
21995 protected function _deactivate_license( $show_notice = true ) {
21996 $this->_logger->entrance();
21997
21998 $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...';
21999
22000 if ( ! FS_Plugin_License::is_valid_id( $this->_site->license_id ) ) {
22001 $this->_admin_notices->add(
22002 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() ),
22003 $hmm_text
22004 );
22005
22006 return;
22007 }
22008
22009 $api = $this->get_api_site_scope();
22010 $license = $api->call( "/licenses/{$this->_site->license_id}.json", 'delete' );
22011
22012 $this->handle_license_deactivation_result( $license, $hmm_text, $show_notice );
22013 }
22014
22015 /**
22016 * @author Leo Fajardo (@leorw)
22017 * @since 2.2.1
22018 *
22019 * @param FS_Plugin_License $license
22020 * @param bool|string $hmm_text
22021 * @param bool $show_notice
22022 */
22023 private function handle_license_deactivation_result( $license, $hmm_text = false, $show_notice = true ) {
22024 if ( isset( $license->error ) ) {
22025 $this->_admin_notices->add(
22026 $this->get_text_inline( 'It looks like the license deactivation failed.', 'license-deactivation-failed-message' ) . '<br> ' .
22027 $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . ' ' . var_export( $license->error, true ),
22028 $hmm_text,
22029 'error'
22030 );
22031
22032 return;
22033 }
22034
22035 // Update license cache.
22036 if ( is_array( $this->_licenses ) ) {
22037 for ( $i = 0, $len = count( $this->_licenses ); $i < $len; $i ++ ) {
22038 if ( $license->id == $this->_licenses[ $i ]->id ) {
22039 $this->_licenses[ $i ] = new FS_Plugin_License( $license );
22040 }
22041 }
22042 }
22043
22044 // Update site plan to default.
22045 $this->_sync_plans();
22046 $this->_site->plan_id = $this->_plans[0]->id;
22047 // Unlink license from site.
22048 $this->_update_site_license( null );
22049
22050 $this->_store_account();
22051
22052 if ( $show_notice ) {
22053 $this->_admin_notices->add(
22054 sprintf( $this->is_only_premium() ?
22055 $this->get_text_inline( 'Your %s license was successfully deactivated.', 'license-deactivation-message_premium-only' ) :
22056 $this->get_text_inline( 'Your license was successfully deactivated, you are back to the %s plan.', 'license-deactivation-message' ),
22057 $this->get_plan_title()
22058 ),
22059 $this->get_text_inline( 'O.K', 'ok' )
22060 );
22061 }
22062
22063 $this->_admin_notices->remove_sticky( array(
22064 'plan_upgraded',
22065 'license_activated',
22066 ) );
22067 }
22068
22069 /**
22070 * Site plan downgrade.
22071 *
22072 * @author Vova Feldman (@svovaf)
22073 * @since 1.0.4
22074 *
22075 * @return object
22076 *
22077 * @uses FS_Api
22078 */
22079 private function _downgrade_site() {
22080 $this->_logger->entrance();
22081
22082 $deactivate_license = fs_request_get_bool( 'deactivate_license' );
22083
22084 $api = $this->get_api_site_scope();
22085 $site = $api->call( 'downgrade.json', 'put', array( 'deactivate_license' => $deactivate_license ) );
22086
22087 $plan_downgraded = false;
22088 $plan = false;
22089 if ( $this->is_api_result_entity( $site ) ) {
22090 $prev_plan_id = $this->_site->plan_id;
22091
22092 // Update new site plan id.
22093 $this->_site->plan_id = $site->plan_id;
22094
22095 $plan = $this->get_plan();
22096 $subscription = $this->_sync_site_subscription( $this->_license );
22097
22098 // Plan downgraded if plan was changed or subscription was cancelled.
22099 $plan_downgraded = ( $plan instanceof FS_Plugin_Plan && $prev_plan_id != $plan->id ) ||
22100 ( is_object( $subscription ) && ! isset( $subscription->error ) && ! $subscription->is_active() );
22101 } else {
22102 // handle different error cases.
22103 $this->handle_license_deactivation_result(
22104 $site,
22105 $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...'
22106 );
22107 }
22108
22109 if ( ! $plan_downgraded ) {
22110 return (object) array(
22111 'error' => (object) array(
22112 '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' )
22113 )
22114 );
22115 }
22116
22117 // Remove previous sticky message about upgrade (if exist).
22118 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
22119
22120 $this->_admin_notices->add(
22121 sprintf( $this->get_text_inline( 'Your subscription was successfully cancelled. Your %s plan license will expire in %s.', 'plan-x-downgraded-message' ),
22122 $plan->title,
22123 human_time_diff( time(), strtotime( $this->_license->expiration ) )
22124 )
22125 );
22126
22127 // Store site updates.
22128 $this->_store_site();
22129
22130 if ( $deactivate_license &&
22131 ! FS_Plugin_License::is_valid_id( $site->license_id )
22132 ) {
22133 if ( $this->_site->is_localhost() ) {
22134 $this->_license->activated_local = max( 0, $this->_license->activated_local - 1 );
22135 } else {
22136 $this->_license->activated = max( 0, $this->_license->activated - 1 );
22137 }
22138
22139 // Handle successful license deactivation result.
22140 $this->handle_license_deactivation_result( $this->_license );
22141 }
22142
22143 return $site;
22144 }
22145
22146 /**
22147 * @author Vova Feldman (@svovaf)
22148 * @since 1.1.8.1
22149 *
22150 * @param bool|string $plan_name
22151 *
22152 * @return bool If trial was successfully started.
22153 */
22154 function start_trial( $plan_name = false ) {
22155 $this->_logger->entrance();
22156
22157 // Alias.
22158 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
22159
22160 if ( $this->is_trial() ) {
22161 // Already in trial mode.
22162 $this->_admin_notices->add(
22163 sprintf( $this->get_text_inline( 'You are already running the %s in a trial mode.', 'in-trial-mode' ), $this->_module_type ),
22164 $oops_text,
22165 'error'
22166 );
22167
22168 return false;
22169 }
22170
22171 if ( $this->_site->is_trial_utilized() ) {
22172 // Trial was already utilized.
22173 $this->_admin_notices->add(
22174 $this->get_text_inline( 'You already utilized a trial before.', 'trial-utilized' ),
22175 $oops_text,
22176 'error'
22177 );
22178
22179 return false;
22180 }
22181
22182 if ( false !== $plan_name ) {
22183 $plan = $this->get_plan_by_name( $plan_name );
22184
22185 if ( false === $plan ) {
22186 // Plan doesn't exist.
22187 $this->_admin_notices->add(
22188 sprintf( $this->get_text_inline( 'Plan %s do not exist, therefore, can\'t start a trial.', 'trial-plan-x-not-exist' ), $plan_name ),
22189 $oops_text,
22190 'error'
22191 );
22192
22193 return false;
22194 }
22195
22196 if ( ! $plan->has_trial() ) {
22197 // Plan doesn't exist.
22198 $this->_admin_notices->add(
22199 sprintf( $this->get_text_inline( 'Plan %s does not support a trial period.', 'plan-x-no-trial' ), $plan_name ),
22200 $oops_text,
22201 'error'
22202 );
22203
22204 return false;
22205 }
22206 } else {
22207 if ( ! $this->has_trial_plan() ) {
22208 // None of the plans have a trial.
22209 $this->_admin_notices->add(
22210 sprintf( $this->get_text_inline( 'None of the %s\'s plans supports a trial period.', 'no-trials' ), $this->_module_type ),
22211 $oops_text,
22212 'error'
22213 );
22214
22215 return false;
22216 }
22217
22218 $plans_with_trial = FS_Plan_Manager::instance()->get_trial_plans( $this->_plans );
22219
22220 $plan = $plans_with_trial[0];
22221 }
22222
22223 $api = $this->get_api_site_scope();
22224 $plan = $api->call( "plans/{$plan->id}/trials.json", 'post' );
22225
22226 if ( ! $this->is_api_result_entity( $plan ) ) {
22227 // Some API error while trying to start the trial.
22228 $this->_admin_notices->add(
22229 $this->get_api_error_message( $plan ),
22230 $oops_text,
22231 'error'
22232 );
22233
22234 return false;
22235 }
22236
22237 // Sync license.
22238 $this->_sync_license();
22239
22240 return $this->is_trial();
22241 }
22242
22243 /**
22244 * Cancel site trial.
22245 *
22246 * @author Vova Feldman (@svovaf)
22247 * @since 1.0.9
22248 *
22249 * @return object
22250 *
22251 * @uses FS_Api
22252 */
22253 private function _cancel_trial() {
22254 $this->_logger->entrance();
22255
22256 if ( ! $this->is_trial() ) {
22257 return (object) array(
22258 'error' => (object) array(
22259 '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' )
22260 )
22261 );
22262 }
22263
22264 $trial_plan = $this->get_trial_plan();
22265
22266 $api = $this->get_api_site_scope();
22267 $site = $api->call( 'trials.json', 'delete' );
22268
22269 $trial_cancelled = false;
22270
22271 if ( $this->is_api_result_entity( $site ) ) {
22272 $prev_trial_ends = $this->_site->trial_ends;
22273
22274 if ( $this->is_paid_trial() ) {
22275 $this->_license->expiration = $site->trial_ends;
22276 $this->_license->is_cancelled = true;
22277 $this->_update_site_license( $this->_license );
22278 $this->_store_licenses();
22279
22280 // Clear subscription reference.
22281 $this->_sync_site_subscription( null );
22282 }
22283
22284 // Update site info.
22285 $this->_site = new FS_Site( $site );
22286
22287 $trial_cancelled = ( $prev_trial_ends != $site->trial_ends );
22288 } else {
22289 // @todo handle different error cases.
22290 }
22291
22292 if ( ! $trial_cancelled ) {
22293 return (object) array(
22294 'error' => (object) array(
22295 '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' )
22296 )
22297 );
22298 }
22299
22300 // Remove previous sticky messages about upgrade or trial (if exist).
22301 $this->_admin_notices->remove_sticky( array(
22302 'trial_started',
22303 'trial_promotion',
22304 'plan_upgraded',
22305 ) );
22306
22307 // Store site updates.
22308 $this->_store_site();
22309
22310 if ( ! $this->is_addon() ||
22311 ! $this->deactivate_premium_only_addon_without_license( true )
22312 ) {
22313 $this->_admin_notices->add(
22314 sprintf( $this->get_text_inline( 'Your %s free trial was successfully cancelled.', 'trial-cancel-message' ), $trial_plan->title )
22315 );
22316 }
22317
22318 return $site;
22319 }
22320
22321 /**
22322 * @author Vova Feldman (@svovaf)
22323 * @since 1.0.6
22324 *
22325 * @param bool|number $plugin_id
22326 *
22327 * @return bool
22328 */
22329 private function _is_addon_id( $plugin_id ) {
22330 return is_numeric( $plugin_id ) && ( $this->get_id() != $plugin_id );
22331 }
22332
22333 /**
22334 * Check if user eligible to download premium version updates.
22335 *
22336 * @author Vova Feldman (@svovaf)
22337 * @since 1.0.6
22338 *
22339 * @return bool
22340 */
22341 private function _can_download_premium() {
22342 return $this->has_any_active_valid_license() ||
22343 ( $this->is_trial() && ! $this->get_trial_plan()->is_free() );
22344 }
22345
22346 /**
22347 *
22348 * @author Vova Feldman (@svovaf)
22349 * @since 1.0.6
22350 *
22351 * @param bool|number $addon_id
22352 * @param string $type "json" or "zip"
22353 *
22354 * @return string
22355 */
22356 private function _get_latest_version_endpoint( $addon_id = false, $type = 'json' ) {
22357
22358 $is_addon = $this->_is_addon_id( $addon_id );
22359
22360 $is_premium = null;
22361 if ( ! $is_addon ) {
22362 $is_premium = ( $this->is_premium() || $this->_can_download_premium() );
22363 } else if ( $this->is_addon_activated( $addon_id ) ) {
22364 $fs_addon = self::get_instance_by_id( $addon_id );
22365 $is_premium = ( $fs_addon->is_premium() || $fs_addon->_can_download_premium() );
22366 }
22367
22368 // If add-on, then append add-on ID.
22369 $endpoint = ( $is_addon ? "/addons/$addon_id" : '' ) .
22370 '/updates/latest.' . $type;
22371
22372 // If add-on and not yet activated, try to fetch based on server licensing.
22373 if ( is_bool( $is_premium ) ) {
22374 $endpoint = add_query_arg( 'is_premium', json_encode( $is_premium ), $endpoint );
22375 }
22376
22377 if ( $this->has_secret_key() ) {
22378 $endpoint = add_query_arg( 'type', 'all', $endpoint );
22379 } else if ( is_object( $this->_site ) && $this->_site->is_beta() ) {
22380 $endpoint = add_query_arg( 'type', 'beta', $endpoint );
22381 }
22382
22383 return $endpoint;
22384 }
22385
22386 /**
22387 * @author Vova Feldman (@svovaf)
22388 * @since 1.0.4
22389 *
22390 * @param bool|number $addon_id
22391 * @param bool $flush Since 1.1.7.3
22392 * @param int $expiration Since 1.2.2.7
22393 * @param bool|string $newer_than Since 2.2.1
22394 * @param bool|string $fetch_readme Since 2.2.1
22395 *
22396 * @return object|false Plugin latest tag info.
22397 */
22398 function _fetch_latest_version(
22399 $addon_id = false,
22400 $flush = true,
22401 $expiration = WP_FS__TIME_24_HOURS_IN_SEC,
22402 $newer_than = false,
22403 $fetch_readme = true
22404 ) {
22405 $this->_logger->entrance();
22406
22407 if ( $this->is_unresolved_clone( true ) ) {
22408 return false;
22409 }
22410
22411 $switch_to_blog_id = null;
22412
22413 /**
22414 * @since 1.1.7.3 Check for plugin updates from Freemius only if opted-in.
22415 * @since 1.1.7.4 Also check updates for add-ons.
22416 */
22417 if (
22418 ( ! $this->is_registered() || ! FS_Permission_Manager::instance( $this )->is_essentials_tracking_allowed() ) &&
22419 ! $this->_is_addon_id( $addon_id )
22420 ) {
22421 if ( ! is_multisite() ) {
22422 return false;
22423 }
22424
22425 $installs_map = $this->get_blog_install_map();
22426
22427 foreach ( $installs_map as $blog_id => $install ) {
22428 if ( ! FS_Permission_Manager::instance( $this )->is_essentials_tracking_allowed( $blog_id ) ) {
22429 continue;
22430 }
22431
22432 /**
22433 * @var FS_Site $install
22434 */
22435 if ( $install->is_trial() ) {
22436 $switch_to_blog_id = $blog_id;
22437 break;
22438 }
22439
22440 if ( FS_Plugin_License::is_valid_id( $install->license_id ) ) {
22441 $license = $this->get_license_by_id( $install->license_id );
22442
22443 if ( is_object( $license ) && $license->is_features_enabled() ) {
22444 $switch_to_blog_id = $blog_id;
22445 break;
22446 }
22447 }
22448 }
22449
22450 if ( is_null( $switch_to_blog_id ) ) {
22451 return false;
22452 }
22453 }
22454
22455 $current_blog_id = is_numeric( $switch_to_blog_id ) ?
22456 get_current_blog_id() :
22457 0;
22458
22459 if ( is_numeric( $switch_to_blog_id ) ) {
22460 $this->switch_to_blog( $switch_to_blog_id );
22461 }
22462
22463 $latest_version_endpoint = $this->_get_latest_version_endpoint( $addon_id, 'json' );
22464
22465 if ( ! empty( $newer_than ) ) {
22466 $latest_version_endpoint = add_query_arg( 'newer_than', $newer_than, $latest_version_endpoint );
22467 }
22468
22469 if ( true === $fetch_readme ) {
22470 $latest_version_endpoint = add_query_arg( 'readme', 'true', $latest_version_endpoint );
22471 }
22472
22473 $tag = $this->get_api_site_or_plugin_scope()->get(
22474 $latest_version_endpoint,
22475 $flush,
22476 $expiration
22477 );
22478
22479 if ( is_numeric( $switch_to_blog_id ) ) {
22480 $this->switch_to_blog( $current_blog_id );
22481 }
22482
22483 $latest_version = ( is_object( $tag ) && isset( $tag->version ) ) ? $tag->version : 'couldn\'t get';
22484
22485 $this->_logger->departure( 'Latest version ' . $latest_version );
22486
22487 return ( is_object( $tag ) && isset( $tag->version ) ) ? $tag : false;
22488 }
22489
22490 #----------------------------------------------------------------------------------
22491 #region Download Plugin
22492 #----------------------------------------------------------------------------------
22493
22494 /**
22495 * Download latest plugin version, based on plan.
22496 *
22497 * Not like _download_latest(), this will redirect the page
22498 * to secure download url to prevent dual download (from FS to WP server,
22499 * and then from WP server to the client / browser).
22500 *
22501 * @author Vova Feldman (@svovaf)
22502 * @since 1.0.9
22503 *
22504 * @param bool|number $plugin_id
22505 *
22506 * @uses FS_Api
22507 * @uses wp_redirect()
22508 */
22509 private function download_latest_directly( $plugin_id = false ) {
22510 $this->_logger->entrance();
22511
22512 wp_redirect( $this->get_latest_download_api_url( $plugin_id ) );
22513 }
22514
22515 /**
22516 * Get latest plugin FS API download URL.
22517 *
22518 * @author Vova Feldman (@svovaf)
22519 * @since 1.0.9
22520 *
22521 * @param bool|number $plugin_id
22522 *
22523 * @return string
22524 */
22525 private function get_latest_download_api_url( $plugin_id = false ) {
22526 $this->_logger->entrance();
22527
22528 $download_api_url = $this->get_api_site_scope()->get_signed_url(
22529 $this->_get_latest_version_endpoint( $plugin_id, 'zip' )
22530 );
22531
22532 return str_replace( 'http:', 'https:', $download_api_url );
22533 }
22534
22535 /**
22536 * Get payment invoice URL.
22537 *
22538 * @author Vova Feldman (@svovaf)
22539 * @since 1.2.0
22540 *
22541 * @param bool|number $payment_id
22542 *
22543 * @return string
22544 */
22545 function _get_invoice_api_url( $payment_id = false ) {
22546 $this->_logger->entrance();
22547
22548 $url = $this->get_api_user_scope()->get_signed_url(
22549 "/payments/{$payment_id}/invoice.pdf"
22550 );
22551
22552 if ( ! fs_starts_with( $url, 'https://' ) ) {
22553 // Always use HTTPS for invoices.
22554 $url = 'https' . substr( $url, 4 );
22555 }
22556
22557 return $url;
22558 }
22559
22560 /**
22561 * Get latest plugin download link.
22562 *
22563 * @author Vova Feldman (@svovaf)
22564 * @since 1.0.9
22565 *
22566 * @param string $label
22567 * @param bool|number $plugin_id
22568 *
22569 * @return string
22570 */
22571 private function get_latest_download_link( $label, $plugin_id = false ) {
22572 return sprintf(
22573 '<a target="_blank" rel="noopener" href="%s">%s</a>',
22574 $this->_get_latest_download_local_url( $plugin_id ),
22575 $label
22576 );
22577 }
22578
22579 /**
22580 * Get latest plugin download local URL.
22581 *
22582 * @author Vova Feldman (@svovaf)
22583 * @since 1.0.9
22584 *
22585 * @param bool|number $plugin_id
22586 *
22587 * @return string
22588 */
22589 function _get_latest_download_local_url( $plugin_id = false ) {
22590 // Add timestamp to protect from caching.
22591 $params = array( 'ts' => WP_FS__SCRIPT_START_TIME );
22592
22593 if ( ! empty( $plugin_id ) ) {
22594 $params['plugin_id'] = $plugin_id;
22595 } else if ( $this->is_addon() ) {
22596 $params['plugin_id'] = $this->get_id();
22597 }
22598
22599 $fs = $this->is_addon() ?
22600 $this->get_parent_instance() :
22601 $this;
22602
22603 return $this->apply_filters( 'download_latest_url', $fs->get_account_url( 'download_latest', $params ) );
22604 }
22605
22606 #endregion Download Plugin ------------------------------------------------------------------
22607
22608 /**
22609 * @author Vova Feldman (@svovaf)
22610 * @since 1.0.4
22611 *
22612 * @uses FS_Api
22613 *
22614 * @param bool $background Hints the method if it's a background updates check. If false, it means that
22615 * was initiated by the admin.
22616 * @param bool|number $plugin_id
22617 * @param bool $flush Since 1.1.7.3
22618 * @param int $expiration Since 1.2.2.7
22619 * @param bool|string $newer_than Since 2.2.1
22620 */
22621 private function check_updates(
22622 $background = false,
22623 $plugin_id = false,
22624 $flush = true,
22625 $expiration = WP_FS__TIME_24_HOURS_IN_SEC,
22626 $newer_than = false
22627 ) {
22628 $this->_logger->entrance();
22629
22630 // Check if there's a newer version for download.
22631 $new_version = $this->_fetch_newer_version( $plugin_id, $flush, $expiration, $newer_than );
22632
22633 $update = null;
22634 if ( is_object( $new_version ) ) {
22635 $update = new FS_Plugin_Tag( $new_version );
22636
22637 if ( ! $background ) {
22638 $this->_admin_notices->add(
22639 sprintf(
22640 /* translators: %s: Numeric version number (e.g. '2.1.9' */
22641 $this->get_text_inline( 'Version %s was released.', 'version-x-released' ) . ' ' . $this->get_text_inline( 'Please download %s.', 'please-download-x' ),
22642 $update->version,
22643 sprintf(
22644 '<a href="%s" target="_blank" rel="noopener">%s</a>',
22645 $this->get_account_url( 'download_latest' ),
22646 sprintf(
22647 /* translators: %s: plan name (e.g. latest "Professional" version) */
22648 $this->get_text_inline( 'the latest %s version here', 'latest-x-version' ),
22649 $this->get_plan_title()
22650 )
22651 )
22652 ),
22653 $this->get_text_inline( 'New', 'new' ) . '!'
22654 );
22655 }
22656 } else if ( false === $new_version && ! $background ) {
22657 $this->_admin_notices->add(
22658 $this->get_text_inline( 'Seems like you got the latest release.', 'you-have-latest' ),
22659 $this->get_text_inline( 'You are all good!', 'you-are-good' )
22660 );
22661 }
22662
22663 $this->_store_update( $update, true, $plugin_id );
22664 }
22665
22666 /**
22667 * @author Vova Feldman (@svovaf)
22668 * @since 1.0.4
22669 *
22670 * @param bool $flush Since 1.1.7.3 add 24 hour cache by default.
22671 *
22672 * @return FS_Plugin[]
22673 *
22674 * @uses FS_Api
22675 */
22676 private function sync_addons( $flush = false ) {
22677 $this->_logger->entrance();
22678
22679 $api = $this->get_api_site_or_plugin_scope();
22680
22681 $path = $this->add_show_pending( '/addons.json?enriched=true&count=50' );
22682
22683 /**
22684 * @since 1.2.1
22685 *
22686 * If there's a cached version of the add-ons and not asking
22687 * for a flush, just use the currently stored add-ons.
22688 */
22689 if ( ! $flush && $api->is_cached( $path ) ) {
22690 $addons = self::get_all_addons();
22691
22692 return isset( $addons[ $this->_plugin->id ] ) ?
22693 $addons[ $this->_plugin->id ] :
22694 array();
22695 }
22696
22697 $result = $api->get( $path, $flush );
22698
22699 $addons = array();
22700 if ( $this->is_api_result_object( $result, 'plugins' ) &&
22701 is_array( $result->plugins )
22702 ) {
22703 for ( $i = 0, $len = count( $result->plugins ); $i < $len; $i ++ ) {
22704 $addons[ $i ] = new FS_Plugin( $result->plugins[ $i ] );
22705 }
22706
22707 $this->_store_addons( $addons, true );
22708 }
22709
22710 return $addons;
22711 }
22712
22713 /**
22714 * Handle user email update.
22715 *
22716 * @author Vova Feldman (@svovaf)
22717 * @since 1.0.3
22718 * @uses FS_Api
22719 *
22720 * @param string $new_email
22721 *
22722 * @return object
22723 */
22724 private function update_email( $new_email ) {
22725 $this->_logger->entrance();
22726
22727 $api = $this->get_api_user_scope();
22728 $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,email,is_verified", 'put', array(
22729 'email' => $new_email,
22730 'after_email_confirm_url' => $this->_get_admin_page_url(
22731 'account',
22732 array( 'fs_action' => 'sync_user' )
22733 ),
22734 ) );
22735
22736 if ( ! isset( $user->error ) ) {
22737 $this->_user->email = $user->email;
22738 $this->_user->is_verified = $user->is_verified;
22739 $this->_store_user();
22740 } else {
22741 // handle different error cases.
22742 }
22743
22744 return $user;
22745 }
22746
22747 #----------------------------------------------------------------------------------
22748 #region API Error Handling
22749 #----------------------------------------------------------------------------------
22750
22751 /**
22752 * @author Vova Feldman (@svovaf)
22753 * @since 1.1.1
22754 *
22755 * @param mixed $result
22756 *
22757 * @return bool Is API result contains an error.
22758 */
22759 private function is_api_error( $result ) {
22760 return FS_Api::is_api_error( $result );
22761 }
22762
22763 /**
22764 * Checks if given API result is a non-empty and not an error object.
22765 *
22766 * @author Vova Feldman (@svovaf)
22767 * @since 1.2.1.5
22768 *
22769 * @param mixed $result
22770 * @param string|null $required_property Optional property we want to verify that is set.
22771 *
22772 * @return bool
22773 */
22774 function is_api_result_object( $result, $required_property = null ) {
22775 return FS_Api::is_api_result_object( $result, $required_property );
22776 }
22777
22778 /**
22779 * Checks if given API result is a non-empty entity object with non-empty ID.
22780 *
22781 * @author Vova Feldman (@svovaf)
22782 * @since 1.2.1.5
22783 *
22784 * @param mixed $result
22785 *
22786 * @return bool
22787 */
22788 private function is_api_result_entity( $result ) {
22789 return FS_Api::is_api_result_entity( $result );
22790 }
22791
22792 #endregion
22793
22794 /**
22795 * Make sure a given argument is an array of a specific type.
22796 *
22797 * @author Vova Feldman (@svovaf)
22798 * @since 1.2.1.5
22799 *
22800 * @param mixed $array
22801 * @param string $class
22802 *
22803 * @return bool
22804 */
22805 private function is_array_instanceof( $array, $class ) {
22806 return ( is_array( $array ) && ( empty( $array ) || $array[0] instanceof $class ) );
22807 }
22808
22809 /**
22810 * Start install ownership change.
22811 *
22812 * @author Vova Feldman (@svovaf)
22813 * @since 1.1.1
22814 * @uses FS_Api
22815 *
22816 * @param string $candidate_email
22817 * @param string $transfer_type
22818 *
22819 * @return bool Is ownership change successfully initiated.
22820 */
22821 private function init_change_owner( $candidate_email, $transfer_type ) {
22822 $this->_logger->entrance();
22823
22824 $installs_info_by_slug_map = $this->get_parent_and_addons_installs_info();
22825 $install_ids = array();
22826
22827 foreach ( $installs_info_by_slug_map as $slug => $install_info ) {
22828 $install = $install_info['install'];
22829
22830 if ( $this->_user->id != $install->user_id ) {
22831 // Skip add-on installs that are not owned by the parent product's install's owner.
22832 continue;
22833 }
22834
22835 $install_ids[ $slug ] = $install->id;
22836 }
22837
22838 $api = $this->get_api_site_scope();
22839 $result = $api->call( "/users/{$this->_user->id}.json", 'put', array(
22840 'email' => $candidate_email,
22841 'transfer_type' => $transfer_type,
22842 'install_ids' => implode( ',', array_values( $install_ids ) ),
22843 'after_confirm_url' => $this->_get_admin_page_url(
22844 'account',
22845 array( 'fs_action' => 'change_owner' )
22846 ),
22847 ) );
22848
22849 return ! $this->is_api_error( $result );
22850 }
22851
22852 /**
22853 * Handle install ownership change.
22854 *
22855 * @author Vova Feldman (@svovaf)
22856 * @since 1.1.1
22857 * @uses FS_Api
22858 *
22859 * @return bool Was ownership change successfully complete.
22860 */
22861 private function complete_change_owner() {
22862 $this->_logger->entrance();
22863
22864 $install_ids = fs_request_get( 'install_ids' );
22865
22866 if ( ! empty( $install_ids ) ) {
22867 $install_ids = explode( ',', $install_ids );
22868
22869 foreach ( $install_ids as $key => $install_id ) {
22870 if ( ! FS_Site::is_valid_id( $install_id ) ) {
22871 unset( $install_ids[ $key ] );
22872 }
22873 }
22874 }
22875
22876 if ( ! is_array( $install_ids ) ) {
22877 $install_ids = array();
22878 }
22879
22880 $user = new FS_User();
22881 $user->id = fs_request_get( 'user_id' );
22882 $user->public_key = fs_request_get( 'user_public_key' );
22883 $user->secret_key = fs_request_get( 'user_secret_key' );
22884
22885 $prev_user = $this->_user;
22886 $this->_user = $user;
22887
22888 $result = $this->get_api_user_scope( true )->get(
22889 "/installs.json?install_ids=" . implode( ',', $install_ids )
22890 );
22891
22892 $current_blog_sites = self::get_all_sites( $this->get_module_type() );
22893
22894 if ( $this->is_api_result_object( $result, 'installs' ) ) {
22895 $site_id_slug_map = array();
22896
22897 foreach ( $current_blog_sites as $slug => $site ) {
22898 $site_id_slug_map[ $site->id ] = $slug;
22899 }
22900
22901 foreach ( $result->installs as $install ) {
22902 $site = new FS_Site( $install );
22903
22904 if ( ! isset( $site_id_slug_map[ $install->id ] ) ) {
22905 continue;
22906 }
22907
22908 $current_blog_sites[ $site_id_slug_map[ $install->id ] ] = clone $site;
22909
22910 if ( $this->_site->id == $site->id ) {
22911 $this->_site = $site;
22912 }
22913 }
22914 }
22915
22916 // Validate install's user and given user.
22917 if ( $user->id != $this->_site->user_id ) {
22918 $this->_user = $prev_user;
22919
22920 return false;
22921 }
22922
22923 $this->set_account_option( 'sites', $current_blog_sites, true );
22924
22925 // Fetch new user information.
22926 $user_result = $this->get_api_user_scope( true )->get();
22927 $user = new FS_User( $user_result );
22928 $this->_user = $user;
22929
22930 $this->_set_account( $user, $this->_site );
22931
22932 $remove_user = true;
22933 $all_modules_sites = self::get_all_modules_sites();
22934
22935 foreach ( $all_modules_sites as $sites_by_module_type ) {
22936 foreach ( $sites_by_module_type as $sites_by_slug ) {
22937 foreach ( $sites_by_slug as $site ) {
22938 if ( $prev_user->id == $site->user_id ) {
22939 $remove_user = false;
22940 break;
22941 }
22942 }
22943
22944 if ( ! $remove_user ) {
22945 break;
22946 }
22947 }
22948
22949 if ( ! $remove_user ) {
22950 break;
22951 }
22952 }
22953
22954 if ( $remove_user ) {
22955 $users = self::get_all_users();
22956
22957 if ( isset( $users[ $prev_user->id ] ) ) {
22958 unset( $users[ $prev_user->id ] );
22959 } else {
22960 // If the prev user wasn't found by the key, iterate over the users collection.
22961 foreach ( $users as $key => $user ) {
22962 if ( $user->id == $prev_user->id ) {
22963 unset( $users[ $key ] );
22964 break;
22965 }
22966 }
22967 }
22968
22969 $this->set_account_option( 'users', $users, true );
22970 }
22971
22972 return true;
22973 }
22974
22975 /**
22976 * Completes ownership change by license.
22977 *
22978 * @author Leo Fajardo (@leorw)
22979 * @since 2.3.2
22980 *
22981 * @param number $user_id
22982 * @param array[string]number $install_ids_by_slug_map
22983 *
22984 */
22985 private function complete_ownership_change_by_license( $user_id, $install_ids_by_slug_map ) {
22986 $this->_logger->entrance();
22987
22988 $this->sync_user_by_current_install( $user_id );
22989
22990 $result = $this->get_api_user_scope( true )->get(
22991 "/installs.json?install_ids=" . implode( ',', $install_ids_by_slug_map )
22992 );
22993
22994 if ( $this->is_api_result_object( $result, 'installs' ) ) {
22995 $sites = self::get_all_sites( $this->get_module_type() );
22996 $install_ids_by_slug_map = array_flip( $install_ids_by_slug_map );
22997
22998 foreach ( $result->installs as $install ) {
22999 $site = new FS_Site( $install );
23000
23001 $sites[ $install_ids_by_slug_map[ $site->id ] ] = clone $site;
23002 }
23003
23004 $this->set_account_option( 'sites', $sites, true );
23005 }
23006 }
23007
23008 /**
23009 * Handle user name update.
23010 *
23011 * @author Vova Feldman (@svovaf)
23012 * @since 1.0.9
23013 * @uses FS_Api
23014 *
23015 * @return object
23016 */
23017 private function update_user_name() {
23018 $this->_logger->entrance();
23019 $name = fs_request_get( 'fs_user_name_' . $this->get_unique_affix(), '' );
23020
23021 $api = $this->get_api_user_scope();
23022 $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,first,last", 'put', array(
23023 'name' => $name,
23024 ) );
23025
23026 if ( ! isset( $user->error ) ) {
23027 $this->_user->first = $user->first;
23028 $this->_user->last = $user->last;
23029 $this->_store_user();
23030 } else {
23031 // handle different error cases.
23032
23033 }
23034
23035 return $user;
23036 }
23037
23038 /**
23039 * Verify user email.
23040 *
23041 * @author Vova Feldman (@svovaf)
23042 * @since 1.0.3
23043 * @uses FS_Api
23044 */
23045 private function verify_email() {
23046 $this->_handle_account_user_sync();
23047
23048 if ( $this->_user->is_verified() ) {
23049 return;
23050 }
23051
23052 $api = $this->get_api_site_scope();
23053 $result = $api->call( "/users/{$this->_user->id}/verify.json", 'put', array(
23054 'after_email_confirm_url' => $this->_get_admin_page_url(
23055 'account',
23056 array( 'fs_action' => 'sync_user' )
23057 )
23058 ) );
23059
23060 if ( ! isset( $result->error ) ) {
23061 $this->_admin_notices->add( sprintf(
23062 $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' ),
23063 sprintf( '<a href="mailto:%1$s">%2$s</a>', esc_url( $this->_user->email ), $this->_user->email )
23064 ) );
23065 } else {
23066 // handle different error cases.
23067
23068 }
23069 }
23070
23071 /**
23072 * @author Vova Feldman (@svovaf)
23073 * @since 1.1.2
23074 *
23075 * @param array $params
23076 * @param bool|null $network
23077 *
23078 * @return string
23079 */
23080 function get_activation_url( $params = array(), $network = null ) {
23081 if ( $this->is_addon() && $this->has_free_plan() ) {
23082 /**
23083 * @author Vova Feldman (@svovaf)
23084 * @since 1.2.1.7 Add-on's activation is the parent's module activation.
23085 */
23086 return $this->get_parent_instance()->get_activation_url( $params );
23087 }
23088
23089 return $this->apply_filters( 'connect_url', $this->_get_admin_page_url( '', $params, $network ) );
23090 }
23091
23092 /**
23093 * @author Vova Feldman (@svovaf)
23094 * @since 1.2.1.5
23095 *
23096 * @param array $params
23097 *
23098 * @return string
23099 */
23100 function get_reconnect_url( $params = array() ) {
23101 $params['fs_action'] = 'reset_anonymous_mode';
23102 $params['fs_unique_affix'] = $this->get_unique_affix();
23103
23104 return $this->get_activation_url( $params );
23105 }
23106
23107 /**
23108 * Get the URL of the page that should be loaded after the user connect
23109 * or skip in the opt-in screen.
23110 *
23111 * @author Vova Feldman (@svovaf)
23112 * @since 1.1.3
23113 *
23114 * @param string $filter Filter name.
23115 * @param array $params Since 1.2.2.7
23116 * @param bool|null $network
23117 *
23118 * @return string
23119 */
23120 function get_after_activation_url( $filter, $params = array(), $network = null ) {
23121 if ( $this->show_opt_in_on_themes_page() &&
23122 ( fs_request_has( 'pending_activation' ) ||
23123 // For cases when the first time path is set, even though it's a WP.org theme.
23124 fs_request_get_bool( $this->get_unique_affix() . '_show_optin' ) )
23125 ) {
23126 $first_time_path = '';
23127 } else {
23128 $first_time_path = $this->_menu->get_first_time_path(
23129 fs_is_network_admin() && $this->_is_network_active
23130 );
23131 }
23132
23133 if ( $this->_is_network_active &&
23134 fs_is_network_admin() &&
23135 ! $this->_menu->has_network_menu() &&
23136 $this->is_network_registered()
23137 ) {
23138 $target_url = $this->get_account_url();
23139 } else {
23140 // Default plugin's page.
23141 $target_url = $this->_get_admin_page_url( '', array(), $network );
23142 }
23143
23144 return add_query_arg( $params, $this->apply_filters(
23145 $filter,
23146 empty( $first_time_path ) ?
23147 $target_url :
23148 $first_time_path
23149 ) );
23150 }
23151
23152 /**
23153 * Handle account page updates / edits / actions.
23154 *
23155 * @author Vova Feldman (@svovaf)
23156 * @since 1.0.2
23157 *
23158 */
23159 private function _handle_account_edits() {
23160 if ( ! $this->is_user_admin() ) {
23161 return;
23162 }
23163
23164 $action = fs_get_action();
23165
23166 if ( empty( $action ) ) {
23167 return;
23168 }
23169
23170 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
23171 $install_id = fs_request_get( 'install_id', '' );
23172
23173 // Alias.
23174 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
23175
23176 $is_network_action = $this->is_network_level_action();
23177 $blog_id = $this->is_network_level_site_specific_action();
23178 $is_parent_plugin_action = ( $plugin_id == $this->get_id() );
23179
23180 if ( is_numeric( $blog_id ) ) {
23181 $this->switch_to_blog( $blog_id );
23182 } else {
23183 $blog_id = '';
23184 }
23185
23186 switch ( $action ) {
23187 case 'opt_in':
23188 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23189
23190 if ( $is_parent_plugin_action ) {
23191 if ( $is_network_action && ! empty( $blog_id ) ) {
23192 if ( ! $this->is_registered() ) {
23193 $this->install_with_user(
23194 $this->get_network_user(),
23195 false,
23196 false,
23197 false,
23198 false
23199 );
23200
23201 $this->_admin_notices->add(
23202 $this->get_text_inline( 'Site successfully opted in.', 'successful-opt-in' ),
23203 $this->get_text_inline( 'Awesome', 'awesome' )
23204 );
23205 }
23206 }
23207 }
23208 break;
23209
23210 case 'toggle_tracking':
23211 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23212
23213 if ( $is_parent_plugin_action ) {
23214 if ( $is_network_action && ! empty( $blog_id ) ) {
23215 if ( $this->is_registered( true ) ) {
23216 if ( $this->is_tracking_prohibited( $blog_id ) ) {
23217 if ( $this->toggle_site_tracking( true, $blog_id ) ) {
23218 $this->_admin_notices->add(
23219 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>" ),
23220 $this->get_text_inline( 'Thank you!', 'thank-you' )
23221 );
23222 }
23223 } else {
23224 if ( $this->toggle_site_tracking( false, $blog_id ) ) {
23225 $install = $this->get_install_by_blog_id( $blog_id );
23226
23227 $this->_admin_notices->add(
23228 sprintf(
23229 $this->get_text_inline( 'Diagnostic data will no longer be sent from %s to %s.', 'opted-out-successfully' ),
23230 self::get_unfiltered_site_url( $blog_id, true ),
23231 "<b>{$this->get_plugin_title()}</b>"
23232 )
23233 );
23234 }
23235 }
23236 }
23237 }
23238 }
23239
23240 break;
23241
23242 case 'delete_account':
23243 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23244
23245 $is_network_deletion = $is_network_action && empty( $blog_id );
23246
23247 if ( $is_parent_plugin_action ) {
23248 // Delete add-on installs if have any.
23249 $installed_addons = $this->get_installed_addons();
23250 foreach ( $installed_addons as $fs_addon ) {
23251 if ( $is_network_deletion ) {
23252 $fs_addon->delete_network_account_event();
23253 } else {
23254 $fs_addon->delete_account_event();
23255 }
23256 }
23257
23258 if ( $is_network_deletion ) {
23259 $this->delete_network_account_event();
23260 } else {
23261 $this->delete_account_event();
23262 }
23263
23264 // Clear user and site.
23265 $this->_site = null;
23266 $this->_user = null;
23267
23268 $this->maybe_set_slug_and_network_menu_exists_flag();
23269
23270 fs_redirect( $this->get_activation_url() );
23271 } else {
23272 if ( $this->is_addon_activated( $plugin_id ) ) {
23273 $fs_addon = self::get_instance_by_id( $plugin_id );
23274
23275 if ( $is_network_deletion ) {
23276 $fs_addon->delete_network_account_event();
23277 } else {
23278 $fs_addon->delete_account_event();
23279 }
23280
23281 fs_redirect( $this->_get_admin_page_url( 'account' ) );
23282 }
23283 }
23284
23285 return;
23286
23287 case 'downgrade_account':
23288 if ( is_numeric( $blog_id ) ) {
23289 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23290 } else {
23291 check_admin_referer( $action );
23292 }
23293
23294 $switch_to_network_install_blog_after_cancellation = (
23295 is_numeric( $blog_id ) &&
23296 $plugin_id == $this->get_id() &&
23297 ! $this->is_trial()
23298 );
23299
23300 $result = $this->cancel_subscription_or_trial( $plugin_id );
23301 if ( $this->is_api_error( $result ) ) {
23302 $this->_admin_notices->add(
23303 $result->error->message,
23304 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
23305 'error'
23306 );
23307 }
23308
23309 if ( $switch_to_network_install_blog_after_cancellation ) {
23310 $this->switch_to_blog( $this->_storage->network_install_blog_id );
23311 }
23312
23313 return;
23314
23315 case 'activate_license':
23316 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23317
23318 $fs = $this;
23319 if ( $plugin_id != $this->get_id() ) {
23320 $fs = $this->is_addon_activated( $plugin_id ) ?
23321 self::get_instance_by_id( $plugin_id ) :
23322 null;
23323 }
23324
23325 if ( is_object( $fs ) ) {
23326 $fs->_activate_license();
23327
23328 /**
23329 * Remove the product ID from `$_REQUEST` so that the syncing of the license for the other products will work properly.
23330 *
23331 * @author Leo Fajardo (@leorw)
23332 * @since 2.4.0
23333 */
23334 unset( $_REQUEST['plugin_id'] );
23335
23336 if ( $this->is_bundle_license_auto_activation_enabled() ) {
23337 $fs->maybe_activate_bundle_license( null, array(), is_numeric( $blog_id ) ? $blog_id : 0 );
23338 }
23339 }
23340
23341 return;
23342
23343 case 'deactivate_license':
23344 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
23345
23346 if ( $plugin_id == $this->get_id() ) {
23347 $this->_deactivate_license();
23348
23349 if ( $this->is_only_premium() ) {
23350 // Clear user and site.
23351 $this->_site = null;
23352 $this->_user = null;
23353
23354 if ( ! $is_network_action ) {
23355 fs_redirect( $this->get_activation_url() );
23356 } else if ( is_numeric( $blog_id ) ) {
23357 $this->switch_to_blog( $this->_storage->network_install_blog_id );
23358 }
23359 }
23360 } else {
23361 if ( $this->is_addon_activated( $plugin_id ) ) {
23362 $fs_addon = self::get_instance_by_id( $plugin_id );
23363 $fs_addon->_deactivate_license();
23364 }
23365 }
23366
23367 return;
23368
23369 case 'check_updates':
23370 check_admin_referer( $action );
23371 $this->check_updates();
23372
23373 return;
23374
23375 case 'change_owner':
23376 $state = fs_request_get( 'state', 'init' );
23377 switch ( $state ) {
23378 case 'init':
23379 $candidate_email = fs_request_get( 'candidate_email' );
23380 $transfer_type = fs_request_get( 'transfer_type' );
23381
23382 if ( $this->init_change_owner( $candidate_email, $transfer_type ) ) {
23383 if ( 'transfer' === $transfer_type ) {
23384 $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>' ) );
23385 } else {
23386 $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>' ) );
23387 }
23388 }
23389 break;
23390 case 'owner_confirmed':
23391 $candidate_email = fs_request_get( 'candidate_email', '' );
23392
23393 $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>' ) );
23394 break;
23395 case 'candidate_confirmed':
23396 if ( $this->complete_change_owner() ) {
23397 $this->_admin_notices->add_sticky(
23398 sprintf( $this->get_text_inline( '%s is the new owner of the account.', 'change-owner-request_candidate-confirmed' ), '<b>' . $this->_user->email . '</b>' ),
23399 'ownership_changed',
23400 $this->get_text_x_inline( 'Congrats', 'as congratulations', 'congrats' ) . '!'
23401 );
23402 } else {
23403 // @todo Handle failed ownership change message.
23404 }
23405 break;
23406 }
23407
23408 return;
23409
23410 case 'update_user_name':
23411 check_admin_referer( 'update_user_name' );
23412
23413 $result = $this->update_user_name();
23414
23415 if ( isset( $result->error ) ) {
23416 $this->_admin_notices->add(
23417 $this->get_text_inline( 'Please provide your full name.', 'name-update-failed-message' ),
23418 $oops_text,
23419 'error'
23420 );
23421 } else {
23422 $this->_admin_notices->add( $this->get_text_inline( 'Your name was successfully updated.', 'name-updated-message' ) );
23423 }
23424
23425 return;
23426
23427 #region Actions that might be called from external links (e.g. email)
23428
23429 case 'cancel_trial':
23430 $result = $this->cancel_subscription_or_trial( $plugin_id );
23431 if ( $this->is_api_error( $result ) ) {
23432 $this->_admin_notices->add(
23433 $result->error->message,
23434 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
23435 'error'
23436 );
23437 }
23438
23439 return;
23440
23441 case 'verify_email':
23442 $this->verify_email();
23443
23444 return;
23445
23446 case 'sync_user':
23447 $this->_handle_account_user_sync();
23448
23449 return;
23450
23451 case $this->get_unique_affix() . '_sync_license':
23452 $this->_sync_license();
23453
23454 return;
23455
23456 case 'download_latest':
23457 $this->download_latest_directly( $plugin_id );
23458
23459 return;
23460
23461 #endregion
23462 }
23463
23464 if ( WP_FS__IS_POST_REQUEST ) {
23465 $properties = array( 'site_secret_key', 'site_id', 'site_public_key' );
23466 foreach ( $properties as $p ) {
23467 if ( 'update_' . $p === $action ) {
23468 check_admin_referer( $action );
23469
23470 $this->_logger->log( $action );
23471
23472 $site_property = substr( $p, strlen( 'site_' ) );
23473 $site_property_value = fs_request_get( 'fs_' . $p . '_' . $this->get_unique_affix(), '' );
23474 $this->get_site()->{$site_property} = $site_property_value;
23475
23476 // Store account after modification.
23477 $this->_store_site();
23478
23479 $this->do_action( 'account_property_edit', 'site', $site_property, $site_property_value );
23480
23481 $this->_admin_notices->add( sprintf(
23482 /* translators: %s: User's account property (e.g. email address, name) */
23483 $this->get_text_inline( 'You have successfully updated your %s.', 'x-updated' ),
23484 '<b>' . str_replace( '_', ' ', $p ) . '</b>'
23485 ) );
23486
23487 return;
23488 }
23489 }
23490 }
23491 }
23492
23493 /**
23494 * Account page resources load.
23495 *
23496 * @author Vova Feldman (@svovaf)
23497 * @since 1.0.6
23498 */
23499 function _account_page_load() {
23500 $this->_logger->entrance();
23501
23502 $this->_logger->info( var_export( $_REQUEST, true ) );
23503
23504 fs_enqueue_local_style( 'fs_account', '/admin/account.css' );
23505
23506 if ( $this->has_addons() ) {
23507 wp_enqueue_script( 'plugin-install' );
23508 add_thickbox();
23509
23510 function fs_addons_body_class( $classes ) {
23511 $classes .= ' plugins-php';
23512
23513 return $classes;
23514 }
23515
23516 add_filter( 'admin_body_class', 'fs_addons_body_class' );
23517 }
23518
23519 if ( $this->has_paid_plan() &&
23520 ! $this->has_any_license() &&
23521 ! $this->is_sync_executed() &&
23522 $this->is_tracking_allowed()
23523 ) {
23524 /**
23525 * If no licenses found and no sync job was executed during the last 24 hours,
23526 * just execute the sync job right away (blocking execution).
23527 *
23528 * @since 1.1.7.3
23529 */
23530 $this->run_manual_sync();
23531 }
23532
23533 $this->_handle_account_edits();
23534
23535 if (
23536 is_object( $this->_license ) &&
23537 $this->_license->user_id == $this->_user->id &&
23538 ! $this->is_whitelabeled( true )
23539 ) {
23540 $this->_admin_notices->add(
23541 sprintf(
23542 $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' ),
23543 sprintf(
23544 '<a href="#" class="fs-toggle-whitelabel-mode">%s</a>',
23545 $this->get_text_inline( 'Click here', 'click-here' )
23546 )
23547 ),
23548 '',
23549 'success',
23550 false,
23551 'license_not_whitelabeled'
23552 );
23553 }
23554
23555 $this->do_action( 'account_page_load_before_departure' );
23556 }
23557
23558 /**
23559 * Renders the "Affiliation" page.
23560 *
23561 * @author Leo Fajardo (@leorw)
23562 * @since 1.2.3
23563 */
23564 function _affiliation_page_render() {
23565 $this->_logger->entrance();
23566
23567 $this->fetch_affiliate_and_terms();
23568
23569 fs_enqueue_local_style( 'fs_affiliation', '/admin/affiliation.css' );
23570
23571 $is_bundle_context = $this->has_bundle_context();
23572
23573 $plugin_title = $this->get_plugin_title();
23574
23575 if ( $is_bundle_context ) {
23576 $plugin_title = $this->plugin_affiliate_terms->plugin_title;
23577
23578 // Add the suffix "Bundle" only if the word is not present in the title itself.
23579 if ( false === mb_stripos( $plugin_title, fs_text_inline( 'Bundle', 'bundle' ) ) ) {
23580 $plugin_title = $this->apply_filters(
23581 'formatted_bundle_title',
23582 $plugin_title . ' ' . fs_text_inline( 'Bundle', 'bundle' )
23583 );
23584 }
23585 }
23586
23587 $vars = array(
23588 'id' => $this->_module_id,
23589 'plugin_title' => $plugin_title,
23590 );
23591 echo $this->apply_filters( "/forms/affiliation.php", fs_get_template( '/forms/affiliation.php', $vars ) );
23592 }
23593
23594
23595 /**
23596 * Render account page.
23597 *
23598 * @author Vova Feldman (@svovaf)
23599 * @since 1.0.0
23600 */
23601 function _account_page_render() {
23602 $this->_logger->entrance();
23603
23604 $template = 'account.php';
23605 $vars = array( 'id' => $this->_module_id );
23606
23607 /**
23608 * Added filter to the template to allow developers wrapping the template
23609 * in custom HTML (e.g. within a wizard/tabs).
23610 *
23611 * @author Vova Feldman (@svovaf)
23612 * @since 1.2.1.6
23613 */
23614 echo $this->apply_filters( "templates/{$template}", fs_get_template( $template, $vars ) );
23615 }
23616
23617 /**
23618 * Render account connect page.
23619 *
23620 * @author Vova Feldman (@svovaf)
23621 * @since 1.0.7
23622 */
23623 function _connect_page_render() {
23624 $this->_logger->entrance();
23625
23626 $vars = array( 'id' => $this->_module_id );
23627
23628 /**
23629 * Added filter to the template to allow developers wrapping the template
23630 * in custom HTML (e.g. within a wizard/tabs).
23631 *
23632 * @author Vova Feldman (@svovaf)
23633 * @since 1.2.1.6
23634 */
23635 echo $this->apply_filters( 'templates/connect.php', fs_get_template( 'connect.php', $vars ) );
23636 }
23637
23638 /**
23639 * Load required resources before add-ons page render.
23640 *
23641 * @author Vova Feldman (@svovaf)
23642 * @since 1.0.6
23643 */
23644 function _addons_page_load() {
23645 $this->_logger->entrance();
23646
23647 fs_enqueue_local_style( 'fs_addons', '/admin/add-ons.css' );
23648
23649 wp_enqueue_script( 'plugin-install' );
23650 add_thickbox();
23651
23652 function fs_addons_body_class( $classes ) {
23653 $classes .= ' plugins-php';
23654
23655 return $classes;
23656 }
23657
23658 add_filter( 'admin_body_class', 'fs_addons_body_class' );
23659
23660 if ( ! $this->is_registered() && $this->is_org_repo_compliant() ) {
23661 $this->_admin_notices->add(
23662 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>' ),
23663 $this->get_text_x_inline( 'Heads up', 'advance notice of something that will need attention.', 'heads-up' ),
23664 'update-nag'
23665 );
23666 }
23667 }
23668
23669 /**
23670 * Render add-ons page.
23671 *
23672 * @author Vova Feldman (@svovaf)
23673 * @since 1.0.6
23674 */
23675 function _addons_page_render() {
23676 $this->_logger->entrance();
23677
23678 $vars = array( 'id' => $this->_module_id );
23679
23680 /**
23681 * Added filter to the template to allow developers wrapping the template
23682 * in custom HTML (e.g. within a wizard/tabs).
23683 *
23684 * @author Vova Feldman (@svovaf)
23685 * @since 1.2.1.6
23686 */
23687 echo $this->apply_filters( 'templates/add-ons.php', fs_get_template( 'add-ons.php', $vars ) );
23688 }
23689
23690 /* Pricing & Upgrade
23691 ------------------------------------------------------------------------------------------------------------------*/
23692 /**
23693 * Render pricing page.
23694 *
23695 * @author Vova Feldman (@svovaf)
23696 * @since 1.0.0
23697 */
23698 function _pricing_page_render() {
23699 $this->_logger->entrance();
23700
23701 $vars = array( 'id' => $this->_module_id );
23702
23703 if ( 'true' === fs_request_get( 'checkout', false ) ) {
23704 echo $this->apply_filters( 'templates/checkout.php', fs_get_template( 'checkout.php', $vars ) );
23705 } else {
23706 echo $this->apply_filters( 'templates/pricing.php', fs_get_template( 'pricing.php', $vars ) );
23707 }
23708 }
23709
23710 /**
23711 * @author Leo Fajardo (@leorw)
23712 * @since 2.3.1
23713 */
23714 function _maybe_add_pricing_ajax_handler() {
23715 if ( ! $this->should_use_external_pricing() ) {
23716 $this->add_ajax_action( 'pricing_ajax_action', array( &$this, '_fs_pricing_ajax_action_handler' ) );
23717 }
23718 }
23719
23720 /**
23721 * @author Leo Fajardo (@leorw)
23722 * @since 2.3.1
23723 */
23724 function _fs_pricing_ajax_action_handler() {
23725 $this->check_ajax_referer( 'pricing_ajax_action' );
23726
23727 $result = null;
23728 $pricing_action = fs_request_get( 'pricing_action' );
23729
23730 switch ( $pricing_action ) {
23731 case 'fetch_pricing_data':
23732 $params = array(
23733 'is_enriched' => true,
23734 'trial' => fs_request_get_bool( 'trial' ),
23735 'sandbox' => fs_request_get( 'sandbox' ),
23736 's_ctx_type' => fs_request_get( 's_ctx_type' ),
23737 's_ctx_id' => fs_request_get( 's_ctx_id' ),
23738 's_ctx_ts' => fs_request_get( 's_ctx_ts' ),
23739 's_ctx_secure' => fs_request_get( 's_ctx_secure' ),
23740 );
23741
23742 $bundle_id = $this->get_bundle_id();
23743 $bundle_public_key = $this->get_bundle_public_key();
23744
23745 $has_bundle_context = ( FS_Plugin::is_valid_id( $bundle_id ) && ! empty( $bundle_public_key ) );
23746
23747 if ( ! $has_bundle_context ) {
23748 $api = $this->get_api_plugin_scope();
23749 } else {
23750 $api = FS_Api::instance(
23751 $bundle_id,
23752 'plugin',
23753 $bundle_id,
23754 $bundle_public_key,
23755 ! $this->is_live(),
23756 false,
23757 $this->get_sdk_version()
23758 );
23759
23760 $params['plugin_id'] = $this->get_id();
23761 $params['plugin_public_key'] = $this->get_public_key();
23762 }
23763
23764 $result = $api->get( 'pricing.json?' . http_build_query( $params ) );
23765 break;
23766 case 'start_trial':
23767 $result = $this->opt_in(
23768 false,
23769 false,
23770 false,
23771 false,
23772 false,
23773 fs_request_get( 'plan_id' )
23774 );
23775 }
23776
23777 if ( is_object( $result ) && $this->is_api_error( $result ) ) {
23778 $this->_logger->api_error( $result );
23779
23780 self::shoot_ajax_failure(
23781 isset( $result->error ) ?
23782 ( is_string( $result->error ) ? $result->error : $result->error->message ) :
23783 var_export( $result, true )
23784 );
23785 }
23786
23787 $this->shoot_ajax_success( $result );
23788 }
23789
23790 #----------------------------------------------------------------------------------
23791 #region Contact Us
23792 #----------------------------------------------------------------------------------
23793
23794 /**
23795 * Render contact-us page.
23796 *
23797 * @author Vova Feldman (@svovaf)
23798 * @since 1.0.3
23799 */
23800 function _contact_page_render() {
23801 $this->_logger->entrance();
23802
23803 $vars = array( 'id' => $this->_module_id );
23804
23805 /**
23806 * Added filter to the template to allow developers wrapping the template
23807 * in custom HTML (e.g. within a wizard/tabs).
23808 *
23809 * @author Vova Feldman (@svovaf)
23810 * @since 2.1.3
23811 */
23812 echo $this->apply_filters( 'templates/contact.php', fs_get_template( 'contact.php', $vars ) );
23813 }
23814
23815 #endregion ------------------------------------------------------------------------
23816
23817 /**
23818 * Hide all admin notices to prevent distractions.
23819 *
23820 * @author Vova Feldman (@svovaf)
23821 * @since 1.0.3
23822 *
23823 * @uses remove_all_actions()
23824 */
23825 private static function _hide_admin_notices() {
23826 remove_all_actions( 'admin_notices' );
23827 remove_all_actions( 'network_admin_notices' );
23828 remove_all_actions( 'all_admin_notices' );
23829 remove_all_actions( 'user_admin_notices' );
23830 }
23831
23832 static function _clean_admin_content_section_hook() {
23833 $hide_admin_notices = true;
23834
23835 if ( fs_request_is_action( 'allow_clone_resolution_notice' ) ) {
23836 check_admin_referer( 'fs_allow_clone_resolution_notice' );
23837
23838 $hide_admin_notices = false;
23839 }
23840
23841 if ( $hide_admin_notices ) {
23842 self::_hide_admin_notices();
23843 }
23844
23845 // Hide footer.
23846 echo '<style>#wpfooter { display: none !important; }</style>';
23847 }
23848
23849 /**
23850 * Attach to admin_head hook to hide all admin notices.
23851 *
23852 * @author Vova Feldman (@svovaf)
23853 * @since 1.0.3
23854 */
23855 static function _clean_admin_content_section() {
23856 add_action( 'admin_head', 'Freemius::_clean_admin_content_section_hook' );
23857 }
23858
23859 /* CSS & JavaScript
23860 ------------------------------------------------------------------------------------------------------------------*/
23861 /* function _enqueue_script($handle, $src) {
23862 $url = plugins_url( substr( WP_FS__DIR_JS, strlen( $this->_plugin_dir_path ) ) . '/assets/js/' . $src );
23863
23864 $this->_logger->entrance( 'script = ' . $url );
23865
23866 wp_enqueue_script( $handle, $url );
23867 }*/
23868
23869 /* SDK
23870 ------------------------------------------------------------------------------------------------------------------*/
23871 private $_user_api;
23872
23873 /**
23874 *
23875 * @author Vova Feldman (@svovaf)
23876 * @since 1.0.2
23877 *
23878 * @param bool $flush
23879 *
23880 * @return FS_Api
23881 */
23882 function get_api_user_scope( $flush = false ) {
23883 if ( ! isset( $this->_user_api ) || $flush ) {
23884 $this->_user_api = $this->get_api_user_scope_by_user( $this->_user );
23885 }
23886
23887 return $this->_user_api;
23888 }
23889
23890 /**
23891 * @author Vova Feldman (@svovaf)
23892 * @since 2.0.0
23893 *
23894 * @param \FS_User $user
23895 *
23896 * @return \FS_Api
23897 */
23898 private function get_api_user_scope_by_user( FS_User $user ) {
23899 return FS_Api::instance(
23900 $this->_module_id,
23901 'user',
23902 $user->id,
23903 $user->public_key,
23904 ! $this->is_live(),
23905 $user->secret_key,
23906 $this->get_sdk_version()
23907 );
23908 }
23909
23910 /**
23911 *
23912 * @author Leo Fajardo (@leorw)
23913 * @since 2.0.0
23914 *
23915 * @param bool $flush
23916 *
23917 * @return FS_Api
23918 */
23919 private function get_current_or_network_user_api_scope( $flush = false ) {
23920 if ( ! $this->_is_network_active ||
23921 ( isset( $this->_user ) && $this->_user instanceof FS_User )
23922 ) {
23923 return $this->get_api_user_scope( $flush );
23924 }
23925
23926 $user = $this->get_current_or_network_user();
23927
23928 $this->_user_api = FS_Api::instance(
23929 $this->_module_id,
23930 'user',
23931 $user->id,
23932 $user->public_key,
23933 ! $this->is_live(),
23934 $user->secret_key,
23935 $this->get_sdk_version()
23936 );
23937
23938 return $this->_user_api;
23939 }
23940
23941 private $_site_api;
23942
23943 /**
23944 *
23945 * @author Vova Feldman (@svovaf)
23946 * @since 1.0.2
23947 *
23948 * @param bool $flush
23949 *
23950 * @return FS_Api
23951 */
23952 private function get_api_site_scope( $flush = false ) {
23953 if ( ! isset( $this->_site_api ) || $flush ) {
23954 $this->_site_api = FS_Api::instance(
23955 $this->_module_id,
23956 'install',
23957 $this->_site->id,
23958 $this->_site->public_key,
23959 ! $this->is_live(),
23960 $this->_site->secret_key,
23961 $this->get_sdk_version(),
23962 self::get_unfiltered_site_url()
23963 );
23964 }
23965
23966 return $this->_site_api;
23967 }
23968
23969 /**
23970 * @author Leo Fajardo (@leorw)
23971 * @since 2.5.0
23972 *
23973 * @param string $path
23974 * @param string $method
23975 * @param array $params
23976 * @param bool $flush_instance
23977 *
23978 * @return array|mixed|string|void
23979 * @throws Freemius_Exception
23980 */
23981 private function api_site_call( $path, $method = 'GET', $params = array(), $flush_instance = false ) {
23982 $result = $this->get_api_site_scope( $flush_instance )->call( $path, $method, $params );
23983
23984 /**
23985 * 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.
23986 *
23987 * @author Leo Fajardo (@leorw)
23988 * @since 2.5.0
23989 */
23990 if (
23991 $this->is_registered() &&
23992 FS_Api::is_api_result_entity( $result ) &&
23993 isset( $result->url )
23994 ) {
23995 $stored_local_url = trailingslashit( $this->_site->url );
23996 $stored_remote_url = trailingslashit( $result->url );
23997
23998 if ( $stored_local_url !== $stored_remote_url ) {
23999 $this->_site->url = $result->url;
24000 $this->_store_site();
24001 }
24002
24003 if ( fs_strip_url_protocol( $stored_remote_url ) !== self::get_unfiltered_site_url( null, true, true ) ) {
24004 FS_Clone_Manager::instance()->maybe_run_clone_resolution();
24005 }
24006 }
24007
24008 return $result;
24009 }
24010
24011 private $_plugin_api;
24012
24013 /**
24014 * Get plugin public API scope.
24015 *
24016 * @author Vova Feldman (@svovaf)
24017 * @since 1.0.7
24018 *
24019 * @return FS_Api
24020 */
24021 function get_api_plugin_scope() {
24022 if ( ! isset( $this->_plugin_api ) ) {
24023 $this->_plugin_api = FS_Api::instance(
24024 $this->_module_id,
24025 'plugin',
24026 $this->_plugin->id,
24027 $this->_plugin->public_key,
24028 ! $this->is_live(),
24029 false,
24030 $this->get_sdk_version()
24031 );
24032 }
24033
24034 return $this->_plugin_api;
24035 }
24036
24037 /**
24038 * Get bundle public API scope.
24039 *
24040 * @author Vova Feldman (@svovaf)
24041 * @since 2.3.1
24042 *
24043 * @return FS_Api
24044 */
24045 function get_api_bundle_scope() {
24046 return FS_Api::instance(
24047 $this->get_bundle_id(),
24048 'plugin',
24049 $this->get_bundle_id(),
24050 $this->get_bundle_public_key(),
24051 ! $this->is_live(),
24052 false,
24053 $this->get_sdk_version()
24054 );
24055 }
24056
24057 /**
24058 * Get site API scope object (fallback to public plugin scope when not registered).
24059 *
24060 * @author Vova Feldman (@svovaf)
24061 * @since 1.0.7
24062 *
24063 * @return FS_Api
24064 */
24065 function get_api_site_or_plugin_scope() {
24066 return $this->is_registered() ?
24067 $this->get_api_site_scope() :
24068 $this->get_api_plugin_scope();
24069 }
24070
24071 /**
24072 * @author Leo Fajardo (@leorw)
24073 * @since 2.2.3.1
24074 *
24075 * @param object $result
24076 */
24077 private function maybe_modify_api_curl_error_message( $result ) {
24078 if (
24079 'cUrlMissing' !== $result->error->type &&
24080 ( 'CurlException' !== $result->error->type || CURLE_COULDNT_CONNECT != $result->error->code ) &&
24081 ( 'HttpRequestFailed' !== $result->error->type || false === strpos( $result->error->message, 'cURL error ' . CURLE_COULDNT_CONNECT ) )
24082 ) {
24083 return;
24084 }
24085
24086 $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' ) .
24087 ' ' .
24088 $this->esc_html_inline(
24089 sprintf(
24090 'Please contact your hosting provider and ask them to whitelist %s for external connection.',
24091 implode(
24092 ', ',
24093 $this->apply_filters( 'api_domains', array(
24094 'api.freemius.com',
24095 'wp.freemius.com'
24096 ) )
24097 )
24098 ),
24099 'connectivity-whitelist'
24100 ) .
24101 ' ' .
24102 sprintf(
24103 $this->esc_html_inline( 'Once you are done, deactivate the %s and activate it again.', 'connectivity-reactivate-module' ),
24104 $this->get_module_type()
24105 );
24106 }
24107
24108 /**
24109 * Show trial promotional notice (if any trial exist).
24110 *
24111 * @author Vova Feldman (@svovaf)
24112 * @since 1.0.9
24113 *
24114 * @param FS_Plugin_Plan[] $plans
24115 */
24116 function _check_for_trial_plans( $plans ) {
24117 /**
24118 * 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.
24119 *
24120 * @author Vova Feldman (@svovaf)
24121 * @since 2.1.2
24122 */
24123 if ( ! is_array( $plans ) && is_object( $plans ) ) {
24124 $plans = array( $plans );
24125 }
24126
24127 if ( ! $this->is_array_instanceof( $plans, 'FS_Plugin_Plan' ) ) {
24128 $plans = array();
24129 }
24130
24131 $this->_storage->has_trial_plan = FS_Plan_Manager::instance()->has_trial_plan( $plans );
24132 }
24133
24134 /**
24135 * During trial promotion the "upgrade" submenu item turns to
24136 * "start trial" to encourage the trial. Since we want to keep
24137 * the same menu item handler and there's no robust way to
24138 * add new arguments to the menu item link's querystring,
24139 * use JavaScript to find the menu item and update the href of
24140 * the link.
24141 *
24142 * @author Vova Feldman (@svovaf)
24143 * @since 1.2.1.5
24144 */
24145 function _fix_start_trial_menu_item_url() {
24146 $template_args = array( 'id' => $this->_module_id );
24147 fs_require_template( 'add-trial-to-pricing.php', $template_args );
24148 }
24149
24150 /**
24151 * Check if module is currently in a trial promotion mode.
24152 *
24153 * @author Vova Feldman (@svovaf)
24154 * @since 1.2.2.7
24155 *
24156 * @return bool
24157 */
24158 function is_in_trial_promotion() {
24159 return $this->_admin_notices->has_sticky( 'trial_promotion' );
24160 }
24161
24162 /**
24163 * Show trial promotional notice (if any trial exist).
24164 *
24165 * @author Vova Feldman (@svovaf)
24166 * @since 1.0.9
24167 *
24168 * @return bool If trial notice added.
24169 */
24170 function _add_trial_notice() {
24171 if ( ! $this->is_user_admin() ) {
24172 return false;
24173 }
24174
24175 if ( ! $this->is_user_in_admin() ) {
24176 return false;
24177 }
24178
24179 if ( $this->_is_network_active ) {
24180 if ( fs_is_network_admin() ) {
24181 // Network level trial is disabled at the moment.
24182 return false;
24183 }
24184
24185 if ( ! $this->is_delegated_connection() ) {
24186 // Only delegated sites should support trials.
24187 return false;
24188 }
24189 }
24190
24191 // Check if trial message is already shown.
24192 if ( $this->is_in_trial_promotion() ) {
24193 add_action( 'admin_footer', array( &$this, '_fix_start_trial_menu_item_url' ) );
24194
24195 $this->_menu->add_counter_to_menu_item( 1, 'fs-trial' );
24196
24197 return false;
24198 }
24199
24200 if ( $this->is_premium() && ! WP_FS__DEV_MODE ) {
24201 // Don't show trial if running the premium code, unless running in DEV mode.
24202 return false;
24203 }
24204
24205 if ( ! $this->has_trial_plan() ) {
24206 // No plans with trial.
24207 return false;
24208 }
24209
24210 if ( ! $this->apply_filters( 'show_trial', true ) ) {
24211 // Developer explicitly asked not to show the trial promo.
24212 return false;
24213 }
24214
24215 if ( $this->is_registered() ) {
24216 // Check if trial already utilized.
24217 if ( $this->_site->is_trial_utilized() ) {
24218 return false;
24219 }
24220
24221 if ( $this->is_paying_or_trial() ) {
24222 // Don't show trial if paying or already in trial.
24223 return false;
24224 }
24225 }
24226
24227 if ( $this->is_activation_mode() || $this->is_pending_activation() ) {
24228 // If not yet opted-in/skipped, or pending activation, don't show trial.
24229 return false;
24230 }
24231
24232 $last_time_trial_promotion_shown = $this->_storage->get( 'trial_promotion_shown', false );
24233 $was_promotion_shown_before = ( false !== $last_time_trial_promotion_shown );
24234
24235 // Show promotion if never shown before and 24 hours after initial activation with FS.
24236 if ( ! $was_promotion_shown_before &&
24237 $this->_storage->install_timestamp > ( time() - $this->apply_filters( 'show_first_trial_after_n_sec', WP_FS__TIME_24_HOURS_IN_SEC ) )
24238 ) {
24239 return false;
24240 }
24241
24242 // OR if promotion was shown before, try showing it every 30 days.
24243 if ( $was_promotion_shown_before &&
24244 $this->apply_filters( 'reshow_trial_after_every_n_sec', 30 * WP_FS__TIME_24_HOURS_IN_SEC ) > time() - $last_time_trial_promotion_shown
24245 ) {
24246 return false;
24247 }
24248
24249 $trial_period = $this->_trial_days;
24250 $require_payment = $this->_is_trial_require_payment;
24251 $trial_url = $this->get_trial_url();
24252 $plans_string = strtolower( $this->get_text_inline( 'Awesome', 'awesome' ) );
24253
24254 if ( $this->is_registered() ) {
24255 // If opted-in, override trial with up to date data from API.
24256 $trial_plans = FS_Plan_Manager::instance()->get_trial_plans( $this->_plans );
24257 $trial_plans_count = count( $trial_plans );
24258
24259 if ( 0 === $trial_plans_count ) {
24260 // If there's no plans with a trial just exit.
24261 return false;
24262 }
24263
24264 /**
24265 * @var FS_Plugin_Plan $paid_plan
24266 */
24267 $paid_plan = $trial_plans[0];
24268 $require_payment = $paid_plan->is_require_subscription;
24269 $trial_period = $paid_plan->trial_period;
24270
24271 $total_paid_plans = count( $this->_plans ) - ( FS_Plan_Manager::instance()->has_free_plan( $this->_plans ) ? 1 : 0 );
24272
24273 if ( $total_paid_plans !== $trial_plans_count ) {
24274 // Not all paid plans have a trial - generate a string of those that have it.
24275 for ( $i = 0; $i < $trial_plans_count; $i ++ ) {
24276 $plans_string .= sprintf(
24277 ' <a href="%s">%s</a>',
24278 $trial_url,
24279 $trial_plans[ $i ]->title
24280 );
24281
24282 if ( $i < $trial_plans_count - 2 ) {
24283 $plans_string .= ', ';
24284 } else if ( $i == $trial_plans_count - 2 ) {
24285 $plans_string .= ' and ';
24286 }
24287 }
24288 }
24289 }
24290
24291 $message = sprintf(
24292 $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' ),
24293 sprintf( '<b>%s</b>', $this->get_plugin_name() ),
24294 $plans_string,
24295 $trial_period
24296 );
24297
24298 // "No Credit-Card Required" or "No Commitment for N Days".
24299 $cc_string = $require_payment ?
24300 sprintf( $this->get_text_inline( 'No commitment for %s days - cancel anytime!', 'no-commitment-for-x-days' ), $trial_period ) :
24301 $this->get_text_inline( 'No credit card required', 'no-cc-required' ) . '!';
24302
24303
24304 // Start trial button.
24305 $button = ' ' . sprintf(
24306 '<a style="margin-left: 10px; vertical-align: super;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
24307 $trial_url,
24308 $this->get_text_x_inline( 'Start free trial', 'call to action', 'start-free-trial' )
24309 );
24310
24311 $this->_admin_notices->add_sticky(
24312 $this->apply_filters( 'trial_promotion_message', "{$message} {$cc_string} {$button}" ),
24313 'trial_promotion',
24314 '',
24315 'promotion'
24316 );
24317
24318 $this->_storage->trial_promotion_shown = WP_FS__SCRIPT_START_TIME;
24319
24320 return true;
24321 }
24322
24323 /**
24324 * Lets users/customers know that the product has an affiliate program.
24325 *
24326 * @author Leo Fajardo (@leorw)
24327 * @since 1.2.2.11
24328 *
24329 * @return bool Returns true if the notice has been added.
24330 */
24331 function _add_affiliate_program_notice() {
24332 if ( ! $this->is_user_admin() ) {
24333 return false;
24334 }
24335
24336 if ( ! $this->is_user_in_admin() ) {
24337 return false;
24338 }
24339
24340 // Check if the notice is already shown.
24341 if ( $this->_admin_notices->has_sticky( 'affiliate_program' ) ) {
24342 return false;
24343 }
24344
24345 if (
24346 // Product has no affiliate program.
24347 ! $this->has_affiliate_program() ||
24348 // User has applied for an affiliate account.
24349 ! empty( $this->_storage->affiliate_application_data )
24350 ) {
24351 return false;
24352 }
24353
24354 if ( ! $this->apply_filters( 'show_affiliate_program_notice', true ) ) {
24355 // Developer explicitly asked not to show the notice about the affiliate program.
24356 return false;
24357 }
24358
24359 if ( $this->is_activation_mode() || $this->is_pending_activation() ) {
24360 // If not yet opted in/skipped, or pending activation, don't show the notice.
24361 return false;
24362 }
24363
24364 $last_time_notice_was_shown = $this->_storage->get( 'affiliate_program_notice_shown', false );
24365 $was_notice_shown_before = ( false !== $last_time_notice_was_shown );
24366
24367 /**
24368 * Do not show the notice if it was already shown before or less than 30 days have passed since the initial
24369 * activation with FS.
24370 */
24371 if ( $was_notice_shown_before ||
24372 $this->_storage->install_timestamp > ( time() - ( WP_FS__TIME_24_HOURS_IN_SEC * 30 ) )
24373 ) {
24374 return false;
24375 }
24376
24377 if ( ! $this->is_paying() &&
24378 FS_Plugin::AFFILIATE_MODERATION_CUSTOMERS == $this->_plugin->affiliate_moderation
24379 ) {
24380 // If the user is not a customer and the affiliate program is only for customers, don't show the notice.
24381 return false;
24382 }
24383
24384 $message = sprintf(
24385 $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' ),
24386 sprintf( '<strong>%s</strong>', $this->get_plugin_name() ),
24387 $this->get_module_label( true )
24388 );
24389
24390 // HTML code for the "Learn more..." button.
24391 $button = ' ' . sprintf(
24392 '<a style="display: block; margin-top: 10px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
24393 $this->_get_admin_page_url( 'affiliation' ),
24394 $this->get_text_inline( 'Learn more', 'learn-more' ) . '...'
24395 );
24396
24397 $this->_admin_notices->add_sticky(
24398 $this->apply_filters( 'affiliate_program_notice', "{$message} {$button}" ),
24399 'affiliate_program',
24400 '',
24401 'promotion'
24402 );
24403
24404 $this->_storage->affiliate_program_notice_shown = WP_FS__SCRIPT_START_TIME;
24405
24406 return true;
24407 }
24408
24409 /**
24410 * @author Vova Feldman (@svovaf)
24411 * @since 1.2.1.5
24412 */
24413 function _enqueue_common_css() {
24414 if ( $this->has_paid_plan() && ! $this->is_paying() ) {
24415 // Add basic CSS for admin-notices and menu-item colors.
24416 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
24417 }
24418 }
24419
24420 /**
24421 * @author Leo Fajardo (@leorw)
24422 * @since 1.2.2
24423 */
24424 function _show_theme_activation_optin_dialog() {
24425 fs_enqueue_local_style( 'fs_connect', '/admin/connect.css' );
24426
24427 add_action( 'admin_footer', array( &$this, '_add_fs_theme_activation_dialog' ) );
24428 }
24429
24430 /**
24431 * @author Leo Fajardo (@leorw)
24432 * @since 1.2.2
24433 */
24434 function _add_fs_theme_activation_dialog() {
24435 global $pagenow;
24436
24437 if ( 'themes.php' !== $pagenow ) {
24438 return;
24439 }
24440
24441 $vars = array( 'id' => $this->_module_id );
24442 fs_require_once_template( 'connect.php', $vars );
24443 }
24444
24445 /* Action Links
24446 ------------------------------------------------------------------------------------------------------------------*/
24447 private $_action_links_hooked = false;
24448 private $_action_links = array();
24449
24450 /**
24451 * Hook to plugin action links filter.
24452 *
24453 * @author Vova Feldman (@svovaf)
24454 * @since 1.0.0
24455 */
24456 private function hook_plugin_action_links() {
24457 $this->_logger->entrance();
24458
24459 $this->_action_links_hooked = true;
24460
24461 $this->_logger->log( 'Adding action links hooks.' );
24462
24463 // Add action link to settings page.
24464 add_filter( 'plugin_action_links_' . $this->_plugin_basename, array(
24465 &$this,
24466 '_modify_plugin_action_links_hook'
24467 ), WP_FS__DEFAULT_PRIORITY, 2 );
24468 add_filter( 'network_admin_plugin_action_links_' . $this->_plugin_basename, array(
24469 &$this,
24470 '_modify_plugin_action_links_hook'
24471 ), WP_FS__DEFAULT_PRIORITY, 2 );
24472 }
24473
24474 /**
24475 * Add plugin action link.
24476 *
24477 * @author Vova Feldman (@svovaf)
24478 * @since 1.0.0
24479 *
24480 * @param $label
24481 * @param $url
24482 * @param bool $external
24483 * @param int $priority
24484 * @param bool $key
24485 */
24486 function add_plugin_action_link( $label, $url, $external = false, $priority = WP_FS__DEFAULT_PRIORITY, $key = false ) {
24487 $this->_logger->entrance();
24488
24489 if ( ! isset( $this->_action_links[ $priority ] ) ) {
24490 $this->_action_links[ $priority ] = array();
24491 }
24492
24493 if ( false === $key ) {
24494 $key = preg_replace( "/[^A-Za-z0-9 ]/", '', strtolower( $label ) );
24495 }
24496
24497 $this->_action_links[ $priority ][] = array(
24498 'label' => $label,
24499 'href' => $url,
24500 'key' => $key,
24501 'external' => $external
24502 );
24503 }
24504
24505 /**
24506 * Adds Upgrade and Add-Ons links to the main Plugins page link actions collection.
24507 *
24508 * @author Vova Feldman (@svovaf)
24509 * @since 1.0.0
24510 */
24511 function _add_upgrade_action_link() {
24512 $this->_logger->entrance();
24513
24514 $is_activation_mode = $this->is_activation_mode();
24515
24516 $add_action_links = $this->should_add_submenu_or_action_links( $is_activation_mode );
24517
24518 /**
24519 * The following logic is based on the logic in `add_submenu_items()` method that decides when the "Upgrade"
24520 * and "Add-Ons" menus should be added.
24521 *
24522 * @author Leo Fajardo (@leorw)
24523 * @since 2.3.0
24524 */
24525 $add_upgrade_link = (
24526 $add_action_links ||
24527 ( $is_activation_mode && $this->is_only_premium() )
24528 ) && ! WP_FS__DEMO_MODE && ( ! $this->is_whitelabeled() );
24529
24530 $add_addons_link = ( $add_action_links && $this->has_addons() );
24531
24532 if ( ! $add_upgrade_link && ! $add_addons_link ) {
24533 return;
24534 }
24535
24536 if (
24537 $add_upgrade_link &&
24538 $this->is_pricing_page_visible() &&
24539 $this->is_submenu_item_visible( 'pricing' )
24540 ) {
24541 $this->add_plugin_action_link(
24542 $this->get_text_inline( 'Upgrade', 'upgrade' ),
24543 $this->get_upgrade_url(),
24544 false,
24545 7,
24546 'upgrade'
24547 );
24548 }
24549
24550 if (
24551 $add_addons_link &&
24552 $this->has_addons() &&
24553 $this->is_submenu_item_visible( 'addons' )
24554 ) {
24555 $this->add_plugin_action_link(
24556 $this->get_text_inline( 'Add-Ons', 'add-ons' ),
24557 $this->_get_admin_page_url( 'addons' ),
24558 false,
24559 9,
24560 'addons'
24561 );
24562 }
24563 }
24564
24565 /**
24566 * Adds "Activate License" or "Change License" link to the main Plugins page link actions collection.
24567 *
24568 * @author Leo Fajardo (@leorw)
24569 * @since 1.1.9
24570 */
24571 function _add_license_action_link() {
24572 $this->_logger->entrance();
24573
24574 if ( ! self::is_ajax() ) {
24575 // Inject license activation dialog UI and client side code.
24576 add_action( 'admin_footer', array( &$this, '_add_license_activation_dialog_box' ) );
24577 }
24578
24579 $link_text = $this->is_free_plan() ?
24580 $this->get_text_inline( 'Activate License', 'activate-license' ) :
24581 $this->get_text_inline( 'Change License', 'change-license' );
24582
24583 $this->add_plugin_action_link(
24584 $link_text,
24585 '#',
24586 false,
24587 11,
24588 ( 'activate-license ' . $this->get_unique_affix() )
24589 );
24590 }
24591
24592 /**
24593 * @author Leo Fajardo (@leorw)
24594 * @since 2.0.2
24595 */
24596 function _add_premium_version_upgrade_selection_action() {
24597 $this->_logger->entrance();
24598
24599 if ( ! self::is_ajax() ) {
24600 add_action( 'admin_footer', array( &$this, '_add_premium_version_upgrade_selection_dialog_box' ) );
24601 }
24602 }
24603
24604 /**
24605 * Adds "Opt In" or "Opt Out" link to the main "Plugins" page link actions collection.
24606 *
24607 * @author Leo Fajardo (@leorw)
24608 * @since 1.2.1.5
24609 */
24610 function _add_tracking_links() {
24611 if ( ! current_user_can( 'manage_options' ) ) {
24612 return;
24613 }
24614
24615 $this->_logger->entrance();
24616
24617 if ( $this->is_only_premium() && $this->is_free_plan() ) {
24618 // 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.
24619 return;
24620 }
24621
24622 if (
24623 $this->is_addon() &&
24624 ! $this->is_only_premium()
24625 ) {
24626 $parent = $this->get_parent_instance();
24627
24628 if ( is_object( $parent ) && $parent->is_anonymous() ) {
24629 return;
24630 }
24631 }
24632
24633 if ( fs_is_network_admin() ) {
24634 if ( ! $this->_is_network_active ) {
24635 // Don't add tracking links when browsing the network WP Admin and the plugin is not network active.
24636 return;
24637 } else if ( $this->is_network_delegated_connection() ) {
24638 // Don't add tracking links when browsing the network WP Admin and the activation has been delegated to site admins.
24639 return;
24640 }
24641 } else {
24642 if ( $this->_is_network_active && ! $this->is_delegated_connection() ) {
24643 // Don't add tracking links when browsing the sub-site WP Admin, the plugin is network active, and the connection was not delegated.
24644 return;
24645 }
24646 }
24647
24648 if ( fs_request_is_action_secure( $this->get_unique_affix() . '_reconnect' ) ) {
24649 if ( ! $this->is_registered() && $this->is_anonymous() ) {
24650 $this->connect_again();
24651
24652 return;
24653 }
24654 }
24655
24656 if ( ( $this->is_plugin() && ! self::is_plugins_page() ) ||
24657 ( $this->is_theme() && ! self::is_themes_page() )
24658 ) {
24659 // Only show tracking links on the plugins and themes pages.
24660 return;
24661 }
24662
24663 if (
24664 $this->is_activation_mode() &&
24665 $this->is_premium() &&
24666 ! $this->is_registered()
24667 ) {
24668 // If not yet registered and running the premium code base, a license activation link will already be shown.
24669 return;
24670 }
24671
24672 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
24673 if ( ! $this->is_premium() && ! $this->is_enable_anonymous() ) {
24674 // If opted in and tracking is allowed, don't allow to opt out if not premium and anonymous mode is disabled.
24675 return;
24676 }
24677 }
24678
24679 if ( $this->add_ajax_action( 'toggle_permission_tracking', array( &$this, '_toggle_permission_tracking_callback' ) ) ) {
24680 return;
24681 }
24682
24683 $link_text_id = '';
24684 $url = '#';
24685
24686 if ( $this->is_registered( true ) ) {
24687 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
24688 $link_text_id = $this->get_text_inline( 'Opt Out', 'opt-out' );
24689 } else {
24690 $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' );
24691 }
24692 } else if ( $this->is_anonymous() || $this->is_activation_mode() ) {
24693 /**
24694 * Show opt-in link only if skipped or in activation mode.
24695 */
24696 $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' );
24697
24698 $params = ! $this->is_anonymous() ?
24699 array() :
24700 array(
24701 'nonce' => wp_create_nonce( $this->get_unique_affix() . '_reconnect' ),
24702 'fs_action' => ( $this->get_unique_affix() . '_reconnect' ),
24703 );
24704
24705 $url = $this->get_activation_url( $params );
24706 }
24707
24708 add_action( 'admin_footer', array( &$this, '_add_optout_dialog' ) );
24709
24710 if ( ! empty( $link_text_id ) && $this->is_plugin() && self::is_plugins_page() ) {
24711 $this->add_plugin_action_link(
24712 $link_text_id,
24713 $url,
24714 false,
24715 13,
24716 "opt-in-or-opt-out {$this->_slug}"
24717 );
24718 }
24719 }
24720
24721 /**
24722 * Get the URL of the page that should be loaded right after the plugin activation.
24723 *
24724 * @author Vova Feldman (@svovaf)
24725 * @since 1.1.7.4
24726 *
24727 * @return string
24728 */
24729 function get_after_plugin_activation_redirect_url() {
24730 $url = false;
24731
24732 if ( ! $this->is_addon() || ! $this->has_free_plan() ) {
24733 $first_time_path = $this->_menu->get_first_time_path(
24734 fs_is_network_admin() && $this->_is_network_active
24735 );
24736
24737 if ( $this->is_activation_mode() ) {
24738 $url = $this->get_activation_url();
24739 } else if ( ! empty( $first_time_path ) ) {
24740 $url = $first_time_path;
24741 } else {
24742 $page = '';
24743 if ( ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
24744 if ( $this->is_network_registered() ) {
24745 $page = 'account';
24746 } else if ( $this->is_pending_activation() || $this->is_network_anonymous() ) {
24747 $this->maybe_set_slug_and_network_menu_exists_flag();
24748 }
24749 }
24750
24751 $url = $this->_get_admin_page_url( $page );
24752 }
24753 } else {
24754 $plugin_fs = false;
24755
24756 if ( $this->is_parent_plugin_installed() ) {
24757 $plugin_fs = self::get_parent_instance();
24758 }
24759
24760 if ( is_object( $plugin_fs ) ) {
24761 if ( ! $plugin_fs->is_registered() ) {
24762 // Forward to parent plugin connect when parent not registered.
24763 $url = $plugin_fs->get_activation_url();
24764 } else {
24765 // Forward to account page.
24766 $url = $plugin_fs->_get_admin_page_url( 'account' );
24767 }
24768 }
24769 }
24770
24771 return $url;
24772 }
24773
24774 /**
24775 * Forward page to activation page.
24776 *
24777 * @author Vova Feldman (@svovaf)
24778 * @since 1.0.3
24779 */
24780 function _redirect_on_activation_hook() {
24781 if ( $this->apply_filters( 'redirect_on_activation', true ) ) {
24782 $url = $this->get_after_plugin_activation_redirect_url();
24783
24784 if ( is_string( $url ) ) {
24785 fs_redirect( $url );
24786 }
24787 }
24788 }
24789
24790 /**
24791 * Modify plugin's page action links collection.
24792 *
24793 * @author Vova Feldman (@svovaf)
24794 * @since 1.0.0
24795 *
24796 * @param array $links
24797 * @param $file
24798 *
24799 * @return array
24800 */
24801 function _modify_plugin_action_links_hook( $links, $file ) {
24802 $this->_logger->entrance();
24803
24804 $passed_deactivate = false;
24805 $deactivate_link = '';
24806 $before_deactivate = array();
24807 $after_deactivate = array();
24808 foreach ( $links as $key => $link ) {
24809 if ( 'deactivate' === $key ) {
24810 $deactivate_link = $link;
24811 $passed_deactivate = true;
24812 continue;
24813 }
24814
24815 if ( ! $passed_deactivate ) {
24816 $before_deactivate[ $key ] = $link;
24817 } else {
24818 $after_deactivate[ $key ] = $link;
24819 }
24820 }
24821
24822 ksort( $this->_action_links );
24823
24824 foreach ( $this->_action_links as $new_links ) {
24825 foreach ( $new_links as $link ) {
24826 $before_deactivate[ $link['key'] ] = '<a href="' . $link['href'] . '"' . ( $link['external'] ? ' target="_blank" rel="noopener"' : '' ) . '>' . $link['label'] . '</a>';
24827 }
24828 }
24829
24830 if ( ! empty( $deactivate_link ) ) {
24831 /**
24832 * This HTML element is used to identify the correct plugin when attaching an event to its Deactivate link.
24833 *
24834 * @since 1.2.1.6 Always show the deactivation feedback form since we added automatic free version deactivation upon premium code activation.
24835 */
24836 $deactivate_link .= '<i class="fs-module-id" data-module-id="' . $this->_module_id . '"></i>';
24837
24838 // Append deactivation link.
24839 $before_deactivate['deactivate'] = $deactivate_link;
24840 }
24841
24842 return array_merge( $before_deactivate, $after_deactivate );
24843 }
24844
24845 /**
24846 * Adds admin message.
24847 *
24848 * @author Vova Feldman (@svovaf)
24849 * @since 1.0.4
24850 *
24851 * @param string $message
24852 * @param string $title
24853 * @param string $type
24854 */
24855 function add_admin_message( $message, $title = '', $type = 'success' ) {
24856 $this->_admin_notices->add( $message, $title, $type );
24857 }
24858
24859 /**
24860 * Adds sticky admin message.
24861 *
24862 * @author Vova Feldman (@svovaf)
24863 * @since 1.1.0
24864 *
24865 * @param string $message
24866 * @param string $id
24867 * @param string $title
24868 * @param string $type
24869 */
24870 function add_sticky_admin_message( $message, $id, $title = '', $type = 'success' ) {
24871 $this->_admin_notices->add_sticky( $message, $id, $title, $type );
24872 }
24873
24874 /**
24875 * Check if the paid version of the module is installed.
24876 *
24877 * @author Vova Feldman (@svovaf)
24878 * @since 2.2.0
24879 *
24880 * @return bool
24881 */
24882 private function is_premium_version_installed() {
24883 $premium_plugin_basename = $this->premium_plugin_basename();
24884
24885 if ( $this->is_theme() ) {
24886 return $this->can_activate_theme( $this->get_premium_slug() );
24887 }
24888
24889 return file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $premium_plugin_basename ) );
24890 }
24891
24892 /**
24893 * Helper function that returns the final steps for the upgrade completion.
24894 *
24895 * If the module is already running the premium code, returns an empty string.
24896 *
24897 * @author Vova Feldman (@svovaf)
24898 * @since 1.2.1
24899 *
24900 * @param string $plan_title
24901 *
24902 * @return string
24903 */
24904 private function get_complete_upgrade_instructions( $plan_title = '' ) {
24905 $this->_logger->entrance();
24906
24907 $activate_license_string = $this->get_license_network_activation_notice();
24908
24909 if ( ! $this->has_premium_version() || $this->is_premium() ) {
24910 return '' . $activate_license_string;
24911 }
24912
24913 if ( empty( $plan_title ) ) {
24914 $plan_title = $this->get_plan_title();
24915 }
24916
24917 if ( $this->is_premium_version_installed() ) {
24918 /**
24919 * If the premium version is already installed, instead of showing the installation instructions,
24920 * tell the current user to activate it.
24921 *
24922 * @author Leo Fajardo (@leorw)
24923 * @since 2.2.1
24924 */
24925 $premium_theme_slug_or_plugin_basename = $this->is_theme() ?
24926 $this->get_premium_slug() :
24927 $this->premium_plugin_basename();
24928
24929 return sprintf(
24930 /* translators: %1$s: Product title; %2$s: Plan title */
24931 $this->get_text_inline( ' The paid version of %1$s is already installed. Please activate it to start benefiting the %2$s features. %3$s', 'activate-premium-version' ),
24932 sprintf( '<em>%s</em>', esc_html( $this->get_plugin_title() ) ),
24933 $plan_title,
24934 sprintf(
24935 '<a style="margin-left: 10px;" href="%s"><button class="button button-primary">%s</button></a>',
24936 ( $this->is_theme() ?
24937 wp_nonce_url( 'themes.php?action=activate&amp;stylesheet=' . $premium_theme_slug_or_plugin_basename, 'switch-theme_' . $premium_theme_slug_or_plugin_basename ) :
24938 wp_nonce_url( 'plugins.php?action=activate&amp;plugin=' . $premium_theme_slug_or_plugin_basename, 'activate-plugin_' . $premium_theme_slug_or_plugin_basename ) ),
24939 esc_html( sprintf(
24940 /* translators: %s: Plan title */
24941 $this->get_text_inline( 'Activate %s features', 'activate-x-features' ),
24942 $plan_title
24943 ) )
24944 )
24945 );
24946 } else {
24947 // @since 1.2.1.5 The free version is auto deactivated.
24948 $deactivation_step = version_compare( $this->version, '1.2.1.5', '<' ) ?
24949 ( '<li>' . $this->esc_html_inline( 'Deactivate the free version', 'deactivate-free-version' ) . '.</li>' ) :
24950 '';
24951
24952 return sprintf(
24953 ' %s: <ol><li>%s.</li>%s<li>%s (<a href="%s" target="_blank" rel="noopener">%s</a>).</li></ol>',
24954 $this->get_text_inline( 'Please follow these steps to complete the upgrade', 'follow-steps-to-complete-upgrade' ),
24955 ( empty( $activate_license_string ) ? '' : $activate_license_string . '</li><li>' ) .
24956 $this->get_latest_download_link( sprintf(
24957 /* translators: %s: Plan title */
24958 $this->get_text_inline( 'Download the latest %s version', 'download-latest-x-version' ),
24959 $plan_title
24960 ) ),
24961 $deactivation_step,
24962 $this->get_text_inline( 'Upload and activate the downloaded version', 'upload-and-activate' ),
24963 $this->apply_filters( 'upload_and_install_video_url', '//bit.ly/wp-' . $this->_module_type . '-upload' ),
24964 $this->get_text_inline( 'How to upload and activate?', 'howto-upload-activate' )
24965 );
24966 }
24967 }
24968
24969 /**
24970 * @author Leo Fajardo (@leorw)
24971 * @since 2.1.0
24972 *
24973 * @param string $url
24974 * @param array $request
24975 */
24976 private static function enrich_request_for_debug( &$url, &$request ) {
24977 if ( WP_FS__DEBUG_SDK || isset( $_COOKIE['XDEBUG_SESSION'] ) ) {
24978 $url = add_query_arg( 'XDEBUG_SESSION_START', rand( 0, 9999999 ), $url );
24979 $url = add_query_arg( 'XDEBUG_SESSION', 'PHPSTORM', $url );
24980
24981 $request['cookies'] = array(
24982 new WP_Http_Cookie( array(
24983 'name' => 'XDEBUG_SESSION',
24984 'value' => 'PHPSTORM',
24985 ) )
24986 );
24987 }
24988 }
24989
24990 /**
24991 * @author Leo Fajardo (@leorw)
24992 * @since 2.1.0
24993 *
24994 * @param string $url
24995 * @param array $request
24996 * @param int $success_cache_expiration
24997 * @param int $failure_cache_expiration
24998 * @param bool $maybe_enrich_request_for_debug
24999 *
25000 * @return WP_Error|array
25001 */
25002 static function safe_remote_post(
25003 &$url,
25004 $request,
25005 $success_cache_expiration = 0,
25006 $failure_cache_expiration = 0,
25007 $maybe_enrich_request_for_debug = true
25008 ) {
25009 $should_cache = ($success_cache_expiration + $failure_cache_expiration > 0);
25010
25011 $cache_key = $should_cache ? md5( fs_strip_url_protocol($url) . json_encode( $request ) ) : false;
25012
25013 $response = (!WP_FS__DEBUG_SDK && ( false !== $cache_key )) ?
25014 get_transient( $cache_key ) :
25015 false;
25016
25017 if ( false === $response ) {
25018 if ( $maybe_enrich_request_for_debug ) {
25019 self::enrich_request_for_debug( $url, $request );
25020 }
25021
25022 $response = wp_remote_post( $url, $request );
25023
25024 if ( $response instanceof WP_Error ) {
25025 if ( 'https://' === substr( $url, 0, 8 ) &&
25026 isset( $response->errors ) &&
25027 isset( $response->errors['http_request_failed'] )
25028 ) {
25029 $http_error = strtolower( $response->errors['http_request_failed'][0] );
25030
25031 if ( false !== strpos( $http_error, 'ssl' ) ||
25032 false !== strpos( $http_error, 'curl error 35' )
25033 ) {
25034 // Failed due to old version of cURL or Open SSL (SSLv3 is not supported by CloudFlare).
25035 $url = 'http://' . substr( $url, 8 );
25036
25037 $request['timeout'] = 15;
25038 $response = wp_remote_post( $url, $request );
25039 }
25040 }
25041 }
25042
25043 if ( false !== $cache_key ) {
25044 set_transient(
25045 $cache_key,
25046 $response,
25047 ( ( $response instanceof WP_Error ) ?
25048 $failure_cache_expiration :
25049 $success_cache_expiration )
25050 );
25051 }
25052 }
25053
25054 return $response;
25055 }
25056
25057 /**
25058 * This method is used to enrich the after upgrade notice instructions when the upgraded
25059 * license cannot be activated network wide (license quota isn't large enough).
25060 *
25061 * @author Vova Feldman (@svovaf)
25062 * @since 2.0.0
25063 *
25064 * @return string
25065 */
25066 private function get_license_network_activation_notice() {
25067 if ( ! $this->_is_network_active ) {
25068 // Module isn't network level activated.
25069 return '';
25070 }
25071
25072 if ( ! fs_is_network_admin() ) {
25073 // Not network level admin.
25074 return '';
25075 }
25076
25077 if ( get_blog_count() == 1 ) {
25078 // There's only a single site in the network so if there's a context license it was already activated.
25079 return '';
25080 }
25081
25082 if ( ! is_object( $this->_license ) ) {
25083 // No context license.
25084 return '';
25085 }
25086
25087 if ( $this->_license->is_single_site() && 0 < $this->_license->activated ) {
25088 // 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).
25089 return '';
25090 }
25091
25092 if ( $this->can_activate_license_on_network( $this->_license ) ) {
25093 // 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).
25094 return '';
25095 }
25096
25097 return sprintf(
25098 $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' ),
25099 '<a href="' . $this->get_account_url( false, array( 'activate_license' => 'true' ) ) . '">',
25100 '</a>'
25101 );
25102 }
25103
25104 /**
25105 * @author Vova Feldman (@svovaf)
25106 * @since 1.2.1.7
25107 *
25108 * @param string $key
25109 *
25110 * @return string
25111 */
25112 function get_text( $key ) {
25113 return fs_text( $key, $this->_slug );
25114 }
25115
25116 /**
25117 * @author Vova Feldman (@svovaf)
25118 * @since 1.2.3
25119 *
25120 * @param string $text Translatable string.
25121 * @param string $key String key for overrides.
25122 *
25123 * @return string
25124 */
25125 function get_text_inline( $text, $key = '' ) {
25126 return _fs_text_inline( $text, $key, $this->_slug );
25127 }
25128
25129 /**
25130 * @author Vova Feldman (@svovaf)
25131 * @since 1.2.3
25132 *
25133 * @param string $text Translatable string.
25134 * @param string $context Context information for the translators.
25135 * @param string $key String key for overrides.
25136 *
25137 * @return string
25138 */
25139 function get_text_x_inline( $text, $context, $key ) {
25140 return _fs_text_x_inline( $text, $context, $key, $this->_slug );
25141 }
25142
25143 /**
25144 * @author Vova Feldman (@svovaf)
25145 * @since 1.2.3
25146 *
25147 * @param string $text Translatable string.
25148 * @param string $key String key for overrides.
25149 *
25150 * @return string
25151 */
25152 function esc_html_inline( $text, $key ) {
25153 return esc_html( _fs_text_inline( $text, $key, $this->_slug ) );
25154 }
25155
25156 #----------------------------------------------------------------------------------
25157 #region Versioning
25158 #----------------------------------------------------------------------------------
25159
25160 /**
25161 * Check if Freemius in SDK upgrade mode.
25162 *
25163 * @author Vova Feldman (@svovaf)
25164 * @since 1.0.9
25165 *
25166 * @return bool
25167 */
25168 function is_sdk_upgrade_mode() {
25169 return isset( $this->_storage->sdk_upgrade_mode ) ?
25170 $this->_storage->sdk_upgrade_mode :
25171 false;
25172 }
25173
25174 /**
25175 * Turn SDK upgrade mode off.
25176 *
25177 * @author Vova Feldman (@svovaf)
25178 * @since 1.0.9
25179 */
25180 function set_sdk_upgrade_complete() {
25181 $this->_storage->sdk_upgrade_mode = false;
25182 }
25183
25184 /**
25185 * Check if plugin upgrade mode.
25186 *
25187 * @author Vova Feldman (@svovaf)
25188 * @since 1.0.9
25189 *
25190 * @return bool
25191 */
25192 function is_plugin_upgrade_mode() {
25193 return isset( $this->_storage->plugin_upgrade_mode ) ?
25194 $this->_storage->plugin_upgrade_mode :
25195 false;
25196 }
25197
25198 /**
25199 * Turn plugin upgrade mode off.
25200 *
25201 * @author Vova Feldman (@svovaf)
25202 * @since 1.0.9
25203 */
25204 function set_plugin_upgrade_complete() {
25205 $this->_storage->plugin_upgrade_mode = false;
25206
25207 $license_migration = ! empty( $this->_storage->license_migration ) ?
25208 $this->_storage->license_migration :
25209 array();
25210
25211 $license_migration['is_migrating'] = false;
25212
25213 $this->_storage->license_migration = $license_migration;
25214 }
25215
25216 #endregion
25217
25218 #----------------------------------------------------------------------------------
25219 #region Permissions
25220 #----------------------------------------------------------------------------------
25221
25222 /**
25223 * Check if specific permission requested.
25224 *
25225 * @author Vova Feldman (@svovaf)
25226 * @since 1.1.6
25227 *
25228 * @param string $permission
25229 *
25230 * @return bool
25231 */
25232 function is_permission_requested( $permission ) {
25233 return isset( $this->_permissions[ $permission ] ) && ( true === $this->_permissions[ $permission ] );
25234 }
25235
25236 #endregion
25237
25238 #----------------------------------------------------------------------------------
25239 #region Auto Activation
25240 #----------------------------------------------------------------------------------
25241
25242 /**
25243 * Hints the SDK if running an auto-installation.
25244 *
25245 * @var bool
25246 */
25247 private $_isAutoInstall = false;
25248
25249 /**
25250 * After upgrade callback to install and auto activate a plugin.
25251 * This code will only be executed on explicit request from the user,
25252 * following the practice Jetpack are using with their theme installations.
25253 *
25254 * @link https://make.wordpress.org/plugins/2017/03/16/clarification-of-guideline-8-executable-code-and-installs/
25255 *
25256 * @author Vova Feldman (@svovaf)
25257 * @since 1.2.1.7
25258 */
25259 function _install_premium_version_ajax_action() {
25260 $this->_logger->entrance();
25261
25262 $this->check_ajax_referer( 'install_premium_version' );
25263
25264 if ( ! $this->is_registered() ) {
25265 // Not registered.
25266 self::shoot_ajax_failure( array(
25267 'message' => $this->get_text_inline( 'Auto installation only works for opted-in users.', 'auto-install-error-not-opted-in' ),
25268 'code' => 'premium_installed',
25269 ) );
25270 }
25271
25272 $plugin_id = fs_request_get( 'target_module_id', $this->get_id() );
25273
25274 if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
25275 // Invalid ID.
25276 self::shoot_ajax_failure( array(
25277 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
25278 'code' => 'invalid_module_id',
25279 ) );
25280 }
25281
25282 if ( $plugin_id == $this->get_id() ) {
25283 if ( $this->is_premium() ) {
25284 // Already using the premium code version.
25285 self::shoot_ajax_failure( array(
25286 'message' => $this->get_text_inline( 'Premium version already active.', 'auto-install-error-premium-activated' ),
25287 'code' => 'premium_installed',
25288 ) );
25289 }
25290 if ( ! $this->can_use_premium_code() ) {
25291 // Don't have access to the premium code.
25292 self::shoot_ajax_failure( array(
25293 'message' => $this->get_text_inline( 'You do not have a valid license to access the premium version.', 'auto-install-error-invalid-license' ),
25294 'code' => 'invalid_license',
25295 ) );
25296 }
25297 if ( ! $this->has_release_on_freemius() ) {
25298 // Plugin is a serviceware, no premium code version.
25299 self::shoot_ajax_failure( array(
25300 'message' => $this->get_text_inline( 'Plugin is a "Serviceware" which means it does not have a premium code version.', 'auto-install-error-serviceware' ),
25301 'code' => 'premium_version_missing',
25302 ) );
25303 }
25304 } else {
25305 $addon = $this->get_addon( $plugin_id );
25306
25307 if ( ! is_object( $addon ) ) {
25308 // Invalid add-on ID.
25309 self::shoot_ajax_failure( array(
25310 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
25311 'code' => 'invalid_module_id',
25312 ) );
25313 }
25314
25315 if ( $this->is_addon_activated( $plugin_id, true ) ) {
25316 // Premium add-on version is already activated.
25317 self::shoot_ajax_failure( array(
25318 'message' => $this->get_text_inline( 'Premium add-on version already installed.', 'auto-install-error-premium-addon-activated' ),
25319 'code' => 'premium_installed',
25320 ) );
25321 }
25322 }
25323
25324 $this->_isAutoInstall = true;
25325
25326 // Try to install and activate.
25327 $updater = FS_Plugin_Updater::instance( $this );
25328 $result = $updater->install_and_activate_plugin( $plugin_id );
25329
25330 if ( is_array( $result ) && ! empty( $result['message'] ) ) {
25331 self::shoot_ajax_failure( array(
25332 'message' => $result['message'],
25333 'code' => $result['code'],
25334 ) );
25335 }
25336
25337 self::shoot_ajax_success( $result );
25338 }
25339
25340 /**
25341 * Displays module activation dialog box after a successful upgrade
25342 * where the user explicitly requested to auto download and install
25343 * the premium version.
25344 *
25345 * @author Vova Feldman (@svovaf)
25346 * @since 1.2.1.7
25347 */
25348 function _add_auto_installation_dialog_box() {
25349 $this->_logger->entrance();
25350
25351 if ( ! $this->is_registered() ) {
25352 // Not registered.
25353 return;
25354 }
25355
25356 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
25357
25358 if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
25359 // Invalid module ID.
25360 return;
25361 }
25362
25363 if ( $plugin_id == $this->get_id() ) {
25364 if ( $this->is_premium() ) {
25365 // Already using the premium code version.
25366 return;
25367 }
25368 if ( ! $this->can_use_premium_code() ) {
25369 // Don't have access to the premium code.
25370 return;
25371 }
25372 if ( ! $this->has_release_on_freemius() ) {
25373 // Plugin is a serviceware, no premium code version.
25374 return;
25375 }
25376 } else {
25377 $addon = $this->get_addon( $plugin_id );
25378
25379 if ( ! is_object( $addon ) ) {
25380 // Invalid add-on ID.
25381 return;
25382 }
25383
25384 if ( $this->is_addon_activated( $plugin_id, true ) ) {
25385 // Premium add-on version is already activated.
25386 return;
25387 }
25388 }
25389
25390 $vars = array(
25391 'id' => $this->_module_id,
25392 'target_module_id' => $plugin_id,
25393 'slug' => $this->_slug,
25394 );
25395
25396 fs_require_template( 'auto-installation.php', $vars );
25397 }
25398
25399 #endregion
25400
25401 #--------------------------------------------------------------------------------
25402 #region Tabs Integration
25403 #--------------------------------------------------------------------------------
25404
25405 #region Module's Original Tabs
25406
25407 /**
25408 * Inject a JavaScript logic to capture the theme tabs HTML.
25409 *
25410 * @author Vova Feldman (@svovaf)
25411 * @since 1.2.2.7
25412 */
25413 function _tabs_capture() {
25414 $this->_logger->entrance();
25415
25416 if (
25417 ! $this->is_product_settings_page() ||
25418 ! $this->should_page_include_tabs() ||
25419 ! $this->is_matching_url( $this->main_menu_url() )
25420 ) {
25421 return;
25422 }
25423
25424 $params = array(
25425 'id' => $this->_module_id,
25426 );
25427
25428 fs_require_once_template( 'tabs-capture-js.php', $params );
25429 }
25430
25431 /**
25432 * Cache theme's tabs HTML for a week. The cache will also be set as expired
25433 * after version and type (free/premium) changes, in addition to the week period.
25434 *
25435 * @author Vova Feldman (@svovaf)
25436 * @since 1.2.2.7
25437 */
25438 function _store_tabs_ajax_action() {
25439 $this->_logger->entrance();
25440
25441 $this->check_ajax_referer( 'store_tabs' );
25442
25443 // Init filesystem if not yet initiated.
25444 WP_Filesystem();
25445
25446 // Get POST body HTML data.
25447 global $wp_filesystem;
25448 $tabs_html = $wp_filesystem->get_contents( "php://input" );
25449
25450 if ( is_string( $tabs_html ) ) {
25451 $tabs_html = trim( $tabs_html );
25452 }
25453
25454 if ( ! is_string( $tabs_html ) || empty( $tabs_html ) ) {
25455 self::shoot_ajax_failure();
25456 }
25457
25458 $this->_cache->set( 'tabs', $tabs_html, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
25459
25460 self::shoot_ajax_success();
25461 }
25462
25463 /**
25464 * Cache theme's settings page custom styles. The cache will also be set as expired
25465 * after version and type (free/premium) changes, in addition to the week period.
25466 *
25467 * @author Vova Feldman (@svovaf)
25468 * @since 1.2.2.7
25469 */
25470 function _store_tabs_styles() {
25471 $this->_logger->entrance();
25472
25473 if (
25474 ! $this->is_product_settings_page() ||
25475 ! $this->should_page_include_tabs() ||
25476 ! $this->is_matching_url( $this->main_menu_url() )
25477 ) {
25478 return;
25479 }
25480
25481 $wp_styles = wp_styles();
25482
25483 $theme_styles_url = get_template_directory_uri();
25484
25485 $stylesheets = array();
25486 foreach ( $wp_styles->queue as $handler ) {
25487 if ( fs_starts_with( $handler, 'fs_' ) ) {
25488 // Assume that stylesheets that their handler starts with "fs_" belong to the SDK.
25489 continue;
25490 }
25491
25492 /**
25493 * @var _WP_Dependency $stylesheet
25494 */
25495 $stylesheet = $wp_styles->registered[ $handler ];
25496
25497 if ( fs_starts_with( $stylesheet->src, $theme_styles_url ) ) {
25498 $stylesheets[] = $stylesheet->src;
25499 }
25500 }
25501
25502 if ( ! empty( $stylesheets ) ) {
25503 $this->_cache->set( 'tabs_stylesheets', $stylesheets, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
25504 }
25505 }
25506
25507 /**
25508 * Check if module's original settings page has any tabs.
25509 *
25510 * @author Vova Feldman (@svovaf)
25511 * @since 1.2.2.7
25512 *
25513 * @return bool
25514 */
25515 private function has_tabs() {
25516 return $this->_cache->has( 'tabs' );
25517 }
25518
25519 /**
25520 * Get module's settings page HTML content, starting
25521 * from the beginning of the <div class="wrap"> element,
25522 * until the tabs HTML (including).
25523 *
25524 * @author Vova Feldman (@svovaf)
25525 * @since 1.2.2.7
25526 *
25527 * @return string
25528 */
25529 private function get_tabs_html() {
25530 $this->_logger->entrance();
25531
25532 return $this->_cache->get( 'tabs' );
25533 }
25534
25535 /**
25536 * Check if page should include tabs.
25537 *
25538 * @author Vova Feldman (@svovaf)
25539 * @since 1.2.2.7
25540 *
25541 * @return bool
25542 */
25543 private function should_page_include_tabs() {
25544 if ( ! $this->has_settings_menu() ) {
25545 // Don't add tabs if no settings at all.
25546 return false;
25547 }
25548
25549 if ( self::NAVIGATION_TABS !== $this->_navigation ) {
25550 // Only add tabs to themes for now.
25551 return false;
25552 }
25553
25554 if ( $this->is_theme() && ! $this->has_paid_plan() && ! $this->has_addons() ) {
25555 // Only add tabs to monetizing themes.
25556 return false;
25557 }
25558
25559 if ( ! $this->is_product_settings_page() ) {
25560 // Only add tabs if browsing one of the product's setting pages.
25561 return false;
25562 }
25563
25564 if ( $this->is_activation_mode() && $this->is_activation_page() ) {
25565 // Don't include tabs in the activation page.
25566 return false;
25567 }
25568
25569 if ( $this->is_admin_page( 'pricing' ) && fs_request_get_bool( 'checkout' ) ) {
25570 // Don't add tabs on checkout page, we want to reduce distractions
25571 // as much as possible.
25572 return false;
25573 }
25574
25575 return true;
25576 }
25577
25578 /**
25579 * Add the tabs HTML before the setting's page content and
25580 * enqueue any required stylesheets.
25581 *
25582 * @author Vova Feldman (@svovaf)
25583 * @since 1.2.2.7
25584 *
25585 * @return bool If tabs were included.
25586 */
25587 function _add_tabs_before_content() {
25588 $this->_logger->entrance();
25589
25590 if ( ! $this->should_page_include_tabs() ) {
25591 return false;
25592 }
25593
25594 /**
25595 * Enqueue the original stylesheets that are included in the
25596 * theme settings page. That way, if the theme settings has
25597 * some custom _styled_ content above the tabs UI, this
25598 * will make sure that the styling is preserved.
25599 */
25600 $stylesheets = $this->_cache->get( 'tabs_stylesheets', array() );
25601 if ( is_array( $stylesheets ) ) {
25602 for ( $i = 0, $len = count( $stylesheets ); $i < $len; $i ++ ) {
25603 wp_enqueue_style( "fs_{$this->_module_id}_tabs_{$i}", $stylesheets[ $i ] );
25604 }
25605 }
25606
25607 // Cut closing </div> tag.
25608 echo substr( trim( $this->get_tabs_html() ), 0, - 6 );
25609
25610 return true;
25611 }
25612
25613 /**
25614 * Add the tabs closing HTML after the setting's page content.
25615 *
25616 * @author Vova Feldman (@svovaf)
25617 * @since 1.2.2.7
25618 *
25619 * @return bool If tabs closing HTML was included.
25620 */
25621 function _add_tabs_after_content() {
25622 $this->_logger->entrance();
25623
25624 if ( ! $this->should_page_include_tabs() ) {
25625 return false;
25626 }
25627
25628 echo '</div>';
25629
25630 return true;
25631 }
25632
25633 #endregion
25634
25635 /**
25636 * Add in-page JavaScript to inject the Freemius tabs into
25637 * the module's setting tabs section.
25638 *
25639 * @author Vova Feldman (@svovaf)
25640 * @since 1.2.2.7
25641 */
25642 function _add_freemius_tabs() {
25643 $this->_logger->entrance();
25644
25645 if ( ! $this->should_page_include_tabs() ) {
25646 return;
25647 }
25648
25649 $params = array( 'id' => $this->_module_id );
25650 fs_require_once_template( 'tabs.php', $params );
25651 }
25652
25653 #endregion
25654
25655 #--------------------------------------------------------------------------------
25656 #region Customizer Integration for Themes
25657 #--------------------------------------------------------------------------------
25658
25659 /**
25660 * @author Vova Feldman (@svovaf)
25661 * @since 1.2.2.7
25662 *
25663 * @param WP_Customize_Manager $customizer
25664 */
25665 function _customizer_register( $customizer ) {
25666 $this->_logger->entrance();
25667
25668 if ( $this->is_pricing_page_visible() ) {
25669 require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-upsell-control.php';
25670
25671 $customizer->add_section( 'freemius_upsell', array(
25672 'title' => '&#9733; ' . $this->get_text_inline( 'View paid features', 'view-paid-features' ),
25673 'priority' => 1,
25674 ) );
25675 $customizer->add_setting( 'freemius_upsell', array(
25676 'sanitize_callback' => 'esc_html',
25677 ) );
25678
25679 $customizer->add_control( new FS_Customizer_Upsell_Control( $customizer, 'freemius_upsell', array(
25680 'fs' => $this,
25681 'section' => 'freemius_upsell',
25682 'priority' => 100,
25683 ) ) );
25684 }
25685
25686 if ( $this->is_page_visible( 'contact' ) || $this->is_page_visible( 'support' ) ) {
25687 require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-support-section.php';
25688
25689 // Main Documentation Link In Customizer Root.
25690 $customizer->add_section( new FS_Customizer_Support_Section( $customizer, 'freemius_support', array(
25691 'fs' => $this,
25692 'priority' => 1000,
25693 ) ) );
25694 }
25695 }
25696
25697 #endregion
25698
25699 /**
25700 * If the theme has a paid version, add some custom
25701 * styling to the theme's premium version (if exists)
25702 * to highlight that it's the premium version of the
25703 * same theme, making it easier for identification
25704 * after the user upgrades and upload it to the site.
25705 *
25706 * @author Vova Feldman (@svovaf)
25707 * @since 1.2.2.7
25708 */
25709 function _style_premium_theme() {
25710 $this->_logger->entrance();
25711
25712 if ( ! self::is_themes_page() ) {
25713 // Only include in the themes page.
25714 return;
25715 }
25716
25717 if ( ! $this->has_paid_plan() ) {
25718 // Only include if has any paid plans.
25719 return;
25720 }
25721
25722 $params = null;
25723 fs_require_once_template( '/js/jquery.content-change.php', $params );
25724
25725 $params = array(
25726 'slug' => $this->_slug,
25727 'id' => $this->_module_id,
25728 );
25729
25730 fs_require_template( '/js/style-premium-theme.php', $params );
25731 }
25732
25733 /**
25734 * This method will return the absolute URL of the module's local icon.
25735 *
25736 * When you are running your plugin or theme on a **localhost** environment, if the icon
25737 * is not found in the local assets folder, try to fetch the icon URL from Freemius. If not set and
25738 * it's a plugin hosted on WordPress.org, try fetching the icon URL from wordpress.org.
25739 * If an icon is found, this method will automatically attempt to download the icon and store it
25740 * in /freemius/assets/img/{slug}.{png|jpg|gif|svg}.
25741 *
25742 * It's important to mention that this method is NOT phoning home since the developer will deploy
25743 * the product with the local icon in the assets folder. The download process just simplifies
25744 * the process for the developer.
25745 *
25746 * @author Vova Feldman (@svovaf)
25747 * @since 2.0.0
25748 *
25749 * @return string
25750 */
25751 function get_local_icon_url() {
25752 global $fs_active_plugins;
25753
25754 /**
25755 * @since 1.1.7.5
25756 */
25757 $local_path = $this->apply_filters( 'plugin_icon', false );
25758
25759 if ( is_string( $local_path ) ) {
25760 $icons = array( $local_path );
25761 } else {
25762 $img_dir = WP_FS__DIR_IMG;
25763
25764 // Locate the main assets folder.
25765 if ( 1 < count( $fs_active_plugins->plugins ) ) {
25766 $plugin_or_theme_img_dir = ( $this->is_plugin() ? WP_PLUGIN_DIR : get_theme_root( get_stylesheet() ) );
25767
25768 foreach ( $fs_active_plugins->plugins as $sdk_path => &$data ) {
25769 if ( $data->plugin_path == $this->get_plugin_basename() ) {
25770 $img_dir = $plugin_or_theme_img_dir
25771 . '/'
25772 /**
25773 * The basename will be `themes` or the basename of a custom themes directory.
25774 *
25775 * @author Leo Fajardo (@leorw)
25776 * @since 2.2.3
25777 */
25778 . str_replace( '../' . basename( $plugin_or_theme_img_dir ) . '/', '', $sdk_path )
25779 . '/assets/img';
25780
25781 break;
25782 }
25783 }
25784 }
25785
25786 // Try to locate the icon in the assets folder.
25787 $icons = glob( fs_normalize_path( $img_dir . "/{$this->_slug}.*" ) );
25788
25789 if ( ! is_array( $icons ) || 0 === count( $icons ) ) {
25790 if ( ! WP_FS__IS_LOCALHOST && $this->is_theme() ) {
25791 $icons = array(
25792 fs_normalize_path( $img_dir . '/theme-icon.png' )
25793 );
25794 } else {
25795 $icon_found = false;
25796 $local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.png" );
25797
25798 if ( ! function_exists( 'get_filesystem_method' ) ) {
25799 require_once ABSPATH . 'wp-admin/includes/file.php';
25800 }
25801
25802 $have_write_permissions = ( 'direct' === get_filesystem_method( array(), fs_normalize_path( $img_dir ) ) );
25803
25804 /**
25805 * IMPORTANT: THIS CODE WILL NEVER RUN AFTER THE PLUGIN IS IN THE REPO.
25806 *
25807 * This code will only be executed once during the testing
25808 * of the plugin in a local environment. The plugin icon file WILL
25809 * already exist in the assets folder when the plugin is deployed to
25810 * the repository.
25811 */
25812 if ( WP_FS__IS_LOCALHOST && $have_write_permissions ) {
25813 // Fetch icon from Freemius.
25814 $icon = $this->fetch_remote_icon_url();
25815
25816 // Fetch icon from WordPress.org.
25817 if ( empty( $icon ) && $this->is_plugin() && $this->is_org_repo_compliant() ) {
25818 if ( ! function_exists( 'plugins_api' ) ) {
25819 require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
25820 }
25821
25822 $plugin_information = plugins_api( 'plugin_information', array(
25823 'slug' => $this->_slug,
25824 'fields' => array(
25825 'sections' => false,
25826 'tags' => false,
25827 'icons' => true
25828 )
25829 ) );
25830
25831 if (
25832 ! is_wp_error( $plugin_information )
25833 && isset( $plugin_information->icons )
25834 && ! empty( $plugin_information->icons )
25835 ) {
25836 /**
25837 * Get the smallest icon.
25838 *
25839 * @author Leo Fajardo (@leorw)
25840 * @since 1.2.2
25841 */
25842 $icon = end( $plugin_information->icons );
25843 }
25844 }
25845
25846 if ( ! empty( $icon ) ) {
25847 if ( 0 !== strpos( $icon, 'http' ) ) {
25848 $icon = 'http:' . $icon;
25849 }
25850
25851 /**
25852 * Get a clean file extension, e.g.: "jpg" and not "jpg?rev=1305765".
25853 *
25854 * @author Leo Fajardo (@leorw)
25855 * @since 1.2.2
25856 */
25857 $ext = pathinfo( strtok( $icon, '?' ), PATHINFO_EXTENSION );
25858
25859 $local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.{$ext}" );
25860
25861 // Try to download the icon.
25862 $icon_found = fs_download_image( $icon, $local_path );
25863 }
25864 }
25865
25866 if ( ! $icon_found ) {
25867 // No icons found, fallback to default icon.
25868 if ( $have_write_permissions ) {
25869 // If have write permissions, copy default icon.
25870 copy( fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" ), $local_path );
25871 } else {
25872 // If doesn't have write permissions, use default icon path.
25873 $local_path = fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" );
25874 }
25875 }
25876
25877 $icons = array( $local_path );
25878 }
25879 }
25880 }
25881
25882 $icon_dir = dirname( $icons[0] );
25883
25884 return fs_img_url( substr( $icons[0], strlen( $icon_dir ) ), $icon_dir );
25885 }
25886
25887 /**
25888 * Fetch module's extended info.
25889 *
25890 * @author Vova Feldman (@svovaf)
25891 * @since 2.0.0
25892 *
25893 * @return object|mixed
25894 */
25895 private function fetch_module_info() {
25896 return $this->get_api_plugin_scope()->get( 'info.json', false, WP_FS__TIME_WEEK_IN_SEC );
25897 }
25898
25899 /**
25900 * Fetch module's remote icon URL.
25901 *
25902 * @author Vova Feldman (@svovaf)
25903 * @since 2.0.0
25904 *
25905 * @return string
25906 */
25907 function fetch_remote_icon_url() {
25908 $info = $this->fetch_module_info();
25909
25910 return ( $this->is_api_result_object( $info, 'icon' ) && is_string( $info->icon ) ) ?
25911 $info->icon :
25912 '';
25913 }
25914
25915 #--------------------------------------------------------------------------------
25916 #region GDPR
25917 #--------------------------------------------------------------------------------
25918
25919 /**
25920 * @author Leo Fajardo (@leorw)
25921 * @since 2.1.0
25922 *
25923 * @return bool
25924 */
25925 function fetch_and_store_current_user_gdpr_anonymously() {
25926 $pong = $this->ping( null, true );
25927
25928 if ( ! $this->get_api_plugin_scope()->is_valid_ping( $pong ) ) {
25929 return false;
25930 } else {
25931 FS_GDPR_Manager::instance()->store_is_required( $pong->is_gdpr_required );
25932
25933 return $pong->is_gdpr_required;
25934 }
25935 }
25936
25937 /**
25938 * @author Leo Fajardo (@leorw)
25939 * @since 2.1.0
25940 *
25941 * @param array $user_plugins
25942 *
25943 * @return string
25944 */
25945 private function get_gdpr_admin_notice_string( $user_plugins ) {
25946 $this->_logger->entrance();
25947
25948 $addons = self::get_all_addons();
25949
25950 foreach ( $user_plugins as $user_plugin ) {
25951 $has_addons = isset( $addons[ $user_plugin->id ] );
25952
25953 if ( WP_FS__MODULE_TYPE_PLUGIN === $user_plugin->type && ! $has_addons ) {
25954 if ( $this->_module_id == $user_plugin->id ) {
25955 $addons = $this->get_addons();
25956 $has_addons = ( ! empty( $addons ) );
25957 } else {
25958 $plugin_api = FS_Api::instance(
25959 $user_plugin->id,
25960 'plugin',
25961 $user_plugin->id,
25962 $user_plugin->public_key,
25963 ! $user_plugin->is_live,
25964 false,
25965 $this->get_sdk_version()
25966 );
25967
25968 $addons_result = $plugin_api->get( '/addons.json?enriched=true', true );
25969
25970 if ( $this->is_api_result_object( $addons_result, 'plugins' ) &&
25971 is_array( $addons_result->plugins ) &&
25972 ! empty( $addons_result->plugins )
25973 ) {
25974 $has_addons = true;
25975 }
25976 }
25977 }
25978
25979 $user_plugin->has_addons = $has_addons;
25980 }
25981
25982 $is_single_parent_product = ( 1 === count( $user_plugins ) );
25983
25984 $multiple_products_text = '';
25985
25986 if ( $is_single_parent_product ) {
25987 $single_parent_product = reset( $user_plugins );
25988
25989 $thank_you = sprintf(
25990 "<span data-plugin-id='%d'>%s</span>",
25991 $single_parent_product->id,
25992 sprintf(
25993 $single_parent_product->has_addons ?
25994 $this->get_text_inline( 'Thank you so much for using %s and its add-ons!', 'thank-you-for-using-product-and-its-addons' ) :
25995 $this->get_text_inline( 'Thank you so much for using %s!', 'thank-you-for-using-product' ),
25996 sprintf('<b><i>%s</i></b>', $single_parent_product->title)
25997 )
25998 );
25999
26000 $already_opted_in = sprintf(
26001 $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' ),
26002 ( WP_FS__MODULE_TYPE_THEME === $single_parent_product->type ) ? WP_FS__MODULE_TYPE_THEME : WP_FS__MODULE_TYPE_PLUGIN
26003 );
26004 } else {
26005 $thank_you = $this->get_text_inline( 'Thank you so much for using our products!', 'thank-you-for-using-products' );
26006 $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' );
26007
26008 $products_and_add_ons = '';
26009 foreach ( $user_plugins as $user_plugin ) {
26010 if ( ! empty( $products_and_add_ons ) ) {
26011 $products_and_add_ons .= ', ';
26012 }
26013
26014 if ( ! $user_plugin->has_addons ) {
26015 $products_and_add_ons .= sprintf(
26016 "<span data-plugin-id='%d'>%s</span>",
26017 $user_plugin->id,
26018 $user_plugin->title
26019 );
26020 } else {
26021 $products_and_add_ons .= sprintf(
26022 "<span data-plugin-id='%d'>%s</span>",
26023 $user_plugin->id,
26024 sprintf(
26025 $this->get_text_inline( '%s and its add-ons', 'product-and-its-addons' ),
26026 $user_plugin->title
26027 )
26028 );
26029 }
26030 }
26031
26032 $multiple_products_text = sprintf(
26033 "<small class='products'><strong>%s:</strong> %s</small>",
26034 $this->get_text_inline( 'Products', 'products' ),
26035 $products_and_add_ons
26036 );
26037 }
26038
26039 $actions = sprintf(
26040 '<ul><li>%s<span class="action-description"> - %s</span></li><li>%s<span class="action-description"> - %s</span></li></ul>',
26041 sprintf('<button class="button button-primary allow-marketing">%s</button>', $this->get_text_inline( 'Yes', 'yes' ) ),
26042 $this->get_text_inline( 'send me security & feature updates, educational content and offers.', 'send-updates' ),
26043 sprintf('<button class="button button-secondary">%s</button>', $this->get_text_inline( 'No', 'no' ) ),
26044 sprintf(
26045 $this->get_text_inline( 'do %sNOT%s send me security & feature updates, educational content and offers.', 'do-not-send-updates' ),
26046 '<span class="underlined">',
26047 '</span>'
26048 )
26049 );
26050
26051 return sprintf(
26052 '%s %s %s',
26053 $thank_you,
26054 $already_opted_in,
26055 sprintf( $this->get_text_inline( 'Due to the new %sEU General Data Protection Regulation (GDPR)%s compliance requirements it is required that you provide your explicit consent, again, confirming that you are onboard :-)', 'due-to-gdpr-compliance-requirements' ), '<a href="https://eugdpr.org/" target="_blank" rel="noopener noreferrer">', '</a>' ) .
26056 '<br><br>' .
26057 '<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>' .
26058 $actions .
26059 ( $is_single_parent_product ? '' : $multiple_products_text )
26060 );
26061 }
26062
26063 /**
26064 * This method is called for opted-in users to fetch the is_marketing_allowed flag of the user for all the
26065 * plugins and themes they've opted in to.
26066 *
26067 * @author Leo Fajardo (@leorw)
26068 * @since 2.1.0
26069 *
26070 * @param string $user_email
26071 * @param string $license_key
26072 * @param array $plugin_ids
26073 * @param string|null $license_key
26074 *
26075 * @return array|false
26076 */
26077 private function fetch_user_marketing_flag_status_by_plugins( $user_email, $license_key, $plugin_ids ) {
26078 $request = array(
26079 'method' => 'POST',
26080 'body' => array(),
26081 'timeout' => WP_FS__DEBUG_SDK ? 60 : 30,
26082 );
26083
26084 if ( is_string( $user_email ) ) {
26085 $request['body']['email'] = $user_email;
26086 } else {
26087 $request['body']['license_key'] = $license_key;
26088 }
26089
26090 $result = array();
26091
26092 $url = WP_FS__ADDRESS . '/action/service/user_plugin/';
26093 $total_plugin_ids = count( $plugin_ids );
26094
26095 $plugin_ids_count_per_request = 10;
26096 for ( $i = 1; $i <= $total_plugin_ids; $i += $plugin_ids_count_per_request ) {
26097 $plugin_ids_set = array_slice( $plugin_ids, $i - 1, $plugin_ids_count_per_request );
26098
26099 $request['body']['plugin_ids'] = $plugin_ids_set;
26100
26101 $response = self::safe_remote_post(
26102 $url,
26103 $request,
26104 WP_FS__TIME_24_HOURS_IN_SEC,
26105 WP_FS__TIME_12_HOURS_IN_SEC
26106 );
26107
26108 if ( ! is_wp_error( $response ) ) {
26109 $decoded = is_string( $response['body'] ) ?
26110 json_decode( $response['body'] ) :
26111 null;
26112
26113 if (
26114 !is_object($decoded) ||
26115 !isset($decoded->success) ||
26116 true !== $decoded->success ||
26117 !isset( $decoded->data ) ||
26118 !is_array( $decoded->data )
26119 ) {
26120 return false;
26121 }
26122
26123 $result = array_merge( $result, $decoded->data );
26124 }
26125 }
26126
26127 return $result;
26128 }
26129
26130 /**
26131 * @author Leo Fajardo (@leorw)
26132 * @since 2.1.0
26133 */
26134 function _maybe_show_gdpr_admin_notice() {
26135 if ( ! $this->is_user_in_admin() ) {
26136 return;
26137 }
26138
26139 if ( ! $this->should_handle_gdpr_admin_notice() ) {
26140 return;
26141 }
26142
26143 if ( ! $this->is_user_admin() ) {
26144 return;
26145 }
26146
26147 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
26148
26149 $lock = FS_User_Lock::instance();
26150
26151 /**
26152 * Try to acquire a 60-sec lock based on the WP user and thread/process ID.
26153 */
26154 if ( ! $lock->try_lock( 60 ) ) {
26155 return;
26156 }
26157
26158 /**
26159 * @var $current_wp_user WP_User
26160 */
26161 $current_wp_user = self::_get_current_wp_user();
26162
26163 /**
26164 * @var FS_User $current_fs_user
26165 */
26166 $current_fs_user = Freemius::_get_user_by_email( $current_wp_user->user_email );
26167
26168 $ten_years_in_sec = 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC;
26169
26170 if ( ! is_object( $current_fs_user ) ) {
26171 // 10-year lock.
26172 $lock->lock( $ten_years_in_sec );
26173
26174 return;
26175 }
26176
26177 $gdpr = FS_GDPR_Manager::instance();
26178
26179 if ( $gdpr->is_opt_in_notice_shown() ) {
26180 // 30-day lock.
26181 $lock->lock( 30 * WP_FS__TIME_24_HOURS_IN_SEC );
26182
26183 return;
26184 }
26185
26186 if ( ! $gdpr->should_show_opt_in_notice() ) {
26187 // 10-year lock.
26188 $lock->lock( $ten_years_in_sec );
26189
26190 return;
26191 }
26192
26193 $last_time_notice_shown = $gdpr->last_time_notice_was_shown();
26194 $was_notice_shown_before = ( false !== $last_time_notice_shown );
26195
26196 if ( $was_notice_shown_before &&
26197 30 * WP_FS__TIME_24_HOURS_IN_SEC > time() - $last_time_notice_shown
26198 ) {
26199 // If the notice was shown before, show it again after 30 days from the last time it was shown.
26200 return;
26201 }
26202
26203 /**
26204 * Find all plugin IDs that were installed by the current admin.
26205 */
26206 $plugin_ids_map = self::get_user_opted_in_module_ids_map( $current_fs_user->id );
26207
26208 if ( empty( $plugin_ids_map )) {
26209 $lock->lock( $ten_years_in_sec );
26210
26211 return;
26212 }
26213
26214 $user_plugins = $this->fetch_user_marketing_flag_status_by_plugins(
26215 $current_fs_user->email,
26216 null,
26217 array_keys( $plugin_ids_map )
26218 );
26219
26220 if ( empty( $user_plugins ) ) {
26221 $lock->lock(
26222 is_array($user_plugins) ?
26223 $ten_years_in_sec :
26224 // Lock for 24-hours on errors.
26225 WP_FS__TIME_24_HOURS_IN_SEC
26226 );
26227
26228 return;
26229 }
26230
26231 $has_unset_marketing_optin = false;
26232
26233 foreach ( $user_plugins as $user_plugin ) {
26234 if ( true == $user_plugin->is_marketing_allowed ) {
26235 unset( $plugin_ids_map[ $user_plugin->plugin_id ] );
26236 }
26237
26238 if ( ! $has_unset_marketing_optin && is_null( $user_plugin->is_marketing_allowed ) ) {
26239 $has_unset_marketing_optin = true;
26240 }
26241 }
26242
26243 if ( empty( $plugin_ids_map ) ||
26244 ( $was_notice_shown_before && ! $has_unset_marketing_optin )
26245 ) {
26246 $lock->lock( $ten_years_in_sec );
26247
26248 return;
26249 }
26250
26251 $modules = array_merge(
26252 array_values( self::maybe_get_entities_account_option( 'plugins', array() ) ),
26253 array_values( self::maybe_get_entities_account_option( 'themes', array() ) )
26254 );
26255
26256 foreach ( $modules as $module ) {
26257 if ( ! FS_Plugin::is_valid_id( $module->parent_plugin_id ) && isset( $plugin_ids_map[ $module->id ] ) ) {
26258 $plugin_ids_map[ $module->id ] = $module;
26259 }
26260 }
26261
26262 $plugin_title = null;
26263 if ( 1 === count( $plugin_ids_map ) ) {
26264 $module = reset( $plugin_ids_map );
26265 $plugin_title = $module->title;
26266 }
26267
26268 $gdpr->add_opt_in_sticky_notice(
26269 $this->get_gdpr_admin_notice_string( $plugin_ids_map ),
26270 $plugin_title
26271 );
26272
26273 $this->add_gdpr_optin_ajax_handler_and_style();
26274
26275 $gdpr->notice_was_just_shown();
26276
26277 // 30-day lock.
26278 $lock->lock( 30 * WP_FS__TIME_24_HOURS_IN_SEC );
26279 }
26280
26281 /**
26282 * Prevents the GDPR opt-in admin notice from being added if the user has already chosen to allow or not allow
26283 * marketing.
26284 *
26285 * @author Leo Fajardo (@leorw)
26286 * @since 2.1.0
26287 */
26288 private function disable_opt_in_notice_and_lock_user() {
26289 FS_GDPR_Manager::instance()->disable_opt_in_notice();
26290
26291 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
26292
26293 // 10-year lock.
26294 FS_User_Lock::instance()->lock( 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC );
26295 }
26296
26297 /**
26298 * @author Leo Fajardo (@leorw)
26299 * @since 2.1.0
26300 */
26301 function _add_gdpr_optin_js() {
26302 $vars = array( 'id' => $this->_module_id );
26303
26304 fs_require_once_template( 'gdpr-optin-js.php', $vars );
26305 }
26306
26307 /**
26308 * @author Leo Fajardo (@leorw)
26309 * @since 2.1.0
26310 */
26311 function enqueue_gdpr_optin_notice_style() {
26312 fs_enqueue_local_style( 'fs_gdpr_optin_notice', '/admin/gdpr-optin-notice.css' );
26313 }
26314
26315 /**
26316 * @author Leo Fajardo (@leorw)
26317 * @since 2.1.0
26318 */
26319 function _maybe_add_gdpr_optin_ajax_handler() {
26320 $this->add_ajax_action( 'fetch_is_marketing_required_flag_value', array( &$this, '_fetch_is_marketing_required_flag_value_ajax_action' ) );
26321
26322 if ( FS_GDPR_Manager::instance()->is_opt_in_notice_shown() ) {
26323 $this->add_gdpr_optin_ajax_handler_and_style();
26324 }
26325 }
26326
26327 /**
26328 * @author Leo Fajardo (@leorw)
26329 * @since 2.1.0
26330 */
26331 function _fetch_is_marketing_required_flag_value_ajax_action() {
26332 $this->_logger->entrance();
26333
26334 $this->check_ajax_referer( 'fetch_is_marketing_required_flag_value' );
26335
26336 $license_key = fs_request_get( 'license_key' );
26337
26338 if ( empty($license_key) ) {
26339 self::shoot_ajax_failure( $this->get_text_inline( 'License key is empty.', 'empty-license-key' ) );
26340 }
26341
26342 $user_plugins = $this->fetch_user_marketing_flag_status_by_plugins(
26343 null,
26344 $license_key,
26345 array( $this->_module_id )
26346 );
26347
26348 if ( ! is_array( $user_plugins ) ||
26349 empty($user_plugins) ||
26350 !isset($user_plugins[0]->plugin_id) ||
26351 $user_plugins[0]->plugin_id != $this->_module_id
26352 ) {
26353 /**
26354 * If faced an error or if the module ID do not match to the current module, ask for GDPR opt-in.
26355 *
26356 * @author Vova Feldman (@svovaf)
26357 */
26358 self::shoot_ajax_success( array(
26359 'is_marketing_allowed' => null,
26360 'license_owner_id' => null
26361 ) );
26362 }
26363
26364 self::shoot_ajax_success( array(
26365 'is_marketing_allowed' => $user_plugins[0]->is_marketing_allowed,
26366 'license_owner_id' => ( isset( $user_plugins[0]->license_owner_id ) ? $user_plugins[0]->license_owner_id : null )
26367 ) );
26368 }
26369
26370 /**
26371 * @author Leo Fajardo (@leorw)
26372 * @since 2.3.2
26373 *
26374 * @param number[] $install_ids
26375 *
26376 * @return array {
26377 * An array of objects containing the installs' licenses owners data.
26378 *
26379 * @property number $id User ID.
26380 * @property string $email User email (can be masked email).
26381 * }
26382 */
26383 private function fetch_installs_licenses_owners_data( $install_ids ) {
26384 $this->_logger->entrance();
26385
26386 $response = $this->get_api_user_scope()->get(
26387 '/licenses_owners.json?install_ids=' . implode( ',', $install_ids )
26388 );
26389
26390 $license_owners = null;
26391
26392 if ( $this->is_api_result_object( $response, 'owners' ) ) {
26393 $license_owners = $response->owners;
26394 }
26395
26396 return $license_owners;
26397 }
26398
26399 /**
26400 * @author Leo Fajardo (@leorw)
26401 * @since 2.1.0
26402 */
26403 private function add_gdpr_optin_ajax_handler_and_style() {
26404 // Add GDPR action AJAX callback.
26405 $this->add_ajax_action( 'gdpr_optin_action', array( &$this, '_gdpr_optin_ajax_action' ) );
26406
26407 add_action( 'admin_footer', array( &$this, '_add_gdpr_optin_js' ) );
26408 add_action( 'admin_enqueue_scripts', array( &$this, 'enqueue_gdpr_optin_notice_style' ) );
26409 }
26410
26411 /**
26412 * @author Leo Fajardo (@leorw)
26413 * @since 2.1.0
26414 */
26415 function _gdpr_optin_ajax_action() {
26416 $this->_logger->entrance();
26417
26418 $this->check_ajax_referer( 'gdpr_optin_action' );
26419
26420 if ( ! fs_request_has( 'is_marketing_allowed' ) || ! fs_request_has( 'plugin_ids' ) ) {
26421 self::shoot_ajax_failure();
26422 }
26423
26424 $current_wp_user = self::_get_current_wp_user();
26425
26426 $plugin_ids = fs_request_get( 'plugin_ids', array() );
26427 if ( ! is_array( $plugin_ids ) || empty( $plugin_ids ) ) {
26428 self::shoot_ajax_failure();
26429 }
26430
26431 $modules = array_merge(
26432 array_values( self::maybe_get_entities_account_option( 'plugins', array() ) ),
26433 array_values( self::maybe_get_entities_account_option( 'themes', array() ) )
26434 );
26435
26436 foreach ( $modules as $key => $module ) {
26437 if ( ! in_array( $module->id, $plugin_ids ) ) {
26438 unset( $modules[ $key ] );
26439 }
26440 }
26441
26442 if ( empty( $modules ) ) {
26443 self::shoot_ajax_failure();
26444 }
26445
26446 $user_api = $this->get_api_user_scope_by_user( Freemius::_get_user_by_email( $current_wp_user->user_email ) );
26447
26448 foreach ( $modules as $module ) {
26449 $user_api->call( "?plugin_id={$module->id}", 'put', array(
26450 'is_marketing_allowed' => ( true == fs_request_get_bool( 'is_marketing_allowed' ) )
26451 ) );
26452 }
26453
26454 FS_GDPR_Manager::instance()->remove_opt_in_notice();
26455
26456 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
26457
26458 // 10-year lock.
26459 FS_User_Lock::instance()->lock( 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC );
26460
26461 self::shoot_ajax_success();
26462 }
26463
26464 /**
26465 * 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.
26466 *
26467 * @author Vova Feldman (@svovaf)
26468 * @since 2.1.0
26469 *
26470 * @return bool
26471 */
26472 private function should_handle_gdpr_admin_notice() {
26473 return $this->apply_filters(
26474 'handle_gdpr_admin_notice',
26475 // Default to false.
26476 false
26477 );
26478 }
26479
26480 #endregion
26481
26482 #----------------------------------------------------------------------------------
26483 #region Marketing
26484 #----------------------------------------------------------------------------------
26485
26486 /**
26487 * Check if current user purchased any other plugins before.
26488 *
26489 * @author Vova Feldman (@svovaf)
26490 * @since 1.0.9
26491 *
26492 * @return bool
26493 */
26494 function has_purchased_before() {
26495 // TODO: Implement has_purchased_before() method.
26496 throw new Exception( 'not implemented' );
26497 }
26498
26499 /**
26500 * Check if current user classified as an agency.
26501 *
26502 * @author Vova Feldman (@svovaf)
26503 * @since 1.0.9
26504 *
26505 * @return bool
26506 */
26507 function is_agency() {
26508 // TODO: Implement is_agency() method.
26509 throw new Exception( 'not implemented' );
26510 }
26511
26512 /**
26513 * Check if current user classified as a developer.
26514 *
26515 * @author Vova Feldman (@svovaf)
26516 * @since 1.0.9
26517 *
26518 * @return bool
26519 */
26520 function is_developer() {
26521 // TODO: Implement is_developer() method.
26522 throw new Exception( 'not implemented' );
26523 }
26524
26525 /**
26526 * Check if current user classified as a business.
26527 *
26528 * @author Vova Feldman (@svovaf)
26529 * @since 1.0.9
26530 *
26531 * @return bool
26532 */
26533 function is_business() {
26534 // TODO: Implement is_business() method.
26535 throw new Exception( 'not implemented' );
26536 }
26537
26538 #endregion
26539
26540 #----------------------------------------------------------------------------------
26541 #region Helper
26542 #----------------------------------------------------------------------------------
26543
26544 /**
26545 * If running with a secret key, assume it's the developer and show pending plans as well.
26546 *
26547 * @author Vova Feldman (@svovaf)
26548 * @since 2.1.2
26549 *
26550 * @param string $path
26551 *
26552 * @return string
26553 */
26554 function add_show_pending( $path ) {
26555 if ( ! $this->has_secret_key() ) {
26556 return $path;
26557 }
26558
26559 return $path . ( false !== strpos( $path, '?' ) ? '&' : '?' ) . 'show_pending=true';
26560 }
26561
26562 #endregion
26563 }
26564