PluginProbe
Tiered Pricing Table for WooCommerce / 2.2.3
Tiered Pricing Table for WooCommerce v2.2.3
7.1.7 7.1.5 7.1.4 7.1.1 6.5.0 6.4.0 6.1.0 trunk 1.0 1.1 2.0 2.0.1 2.0.2 2.1 2.1.1 2.1.2 2.2.0 2.2.1 2.2.2 2.2.3 2.3.0 2.3.1 2.3.2 2.3.3 2.3.4 All 90 releases
tier-pricing-table / freemius / includes / class-freemius.php

class-freemius.php in Tiered Pricing Table for WooCommerce 2.2.3, at freemius/includes/class-freemius.php

23,284 lines 874.5 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 1.1.6
169 * @var string[]bool.
170 */
171 private $_permissions;
172
173 /**
174 * @var FS_Storage
175 */
176 private $_storage;
177
178 /**
179 * @since 1.2.2.7
180 * @var FS_Cache_Manager
181 */
182 private $_cache;
183
184 /**
185 * @since 1.0.0
186 *
187 * @var FS_Logger
188 */
189 private $_logger;
190 /**
191 * @since 1.0.4
192 *
193 * @var FS_Plugin
194 */
195 private $_plugin = false;
196 /**
197 * @since 1.0.4
198 *
199 * @var FS_Plugin|false
200 */
201 private $_parent_plugin = false;
202 /**
203 * @since 1.1.1
204 *
205 * @var Freemius
206 */
207 private $_parent = false;
208 /**
209 * @since 1.0.1
210 *
211 * @var FS_User
212 */
213 private $_user = false;
214 /**
215 * @since 1.0.1
216 *
217 * @var FS_Site
218 */
219 private $_site = false;
220 /**
221 * @since 1.0.1
222 *
223 * @var FS_Plugin_License
224 */
225 private $_license;
226 /**
227 * @since 1.0.2
228 *
229 * @var FS_Plugin_Plan[]
230 */
231 private $_plans = false;
232 /**
233 * @var FS_Plugin_License[]
234 * @since 1.0.5
235 */
236 private $_licenses = false;
237
238 /**
239 * @since 1.0.1
240 *
241 * @var FS_Admin_Menu_Manager
242 */
243 private $_menu;
244
245 /**
246 * @var FS_Admin_Notices
247 */
248 private $_admin_notices;
249
250 /**
251 * @since 1.1.6
252 *
253 * @var FS_Admin_Notices
254 */
255 private static $_global_admin_notices;
256
257 /**
258 * @var FS_Logger
259 * @since 1.0.0
260 */
261 private static $_static_logger;
262
263 /**
264 * @var FS_Options
265 * @since 1.0.2
266 */
267 private static $_accounts;
268
269 /**
270 * @since 1.2.2
271 *
272 * @var number
273 */
274 private $_module_id;
275
276 /**
277 * @var Freemius[]
278 */
279 private static $_instances = array();
280
281 /**
282 * @since 1.2.3
283 *
284 * @var FS_Affiliate
285 */
286 private $affiliate = null;
287
288 /**
289 * @since 1.2.3
290 *
291 * @var FS_AffiliateTerms
292 */
293 private $plugin_affiliate_terms = null;
294
295 /**
296 * @since 1.2.3
297 *
298 * @var FS_AffiliateTerms
299 */
300 private $custom_affiliate_terms = null;
301
302 /**
303 * @since 2.0.0
304 *
305 * @var bool
306 */
307 private $_is_multisite_integrated;
308
309 /**
310 * @since 2.0.0
311 *
312 * @var bool True if the current request is for a network admin screen and the plugin is network active.
313 */
314 private $_is_network_active;
315
316 /**
317 * @since 2.0.0
318 *
319 * @var int|null The original blog ID the plugin was loaded with.
320 */
321 private $_blog_id = null;
322
323 /**
324 * @since 2.0.0
325 *
326 * @var int|null The current execution context. When true, run on network context. When int, run on the specified blog context.
327 */
328 private $_context_is_network_or_blog_id = null;
329
330 /**
331 * @since 2.0.0
332 *
333 * @var string
334 */
335 private $_dynamically_added_top_level_page_hook_name = '';
336
337 #region Uninstall Reasons IDs
338
339 const REASON_NO_LONGER_NEEDED = 1;
340 const REASON_FOUND_A_BETTER_PLUGIN = 2;
341 const REASON_NEEDED_FOR_A_SHORT_PERIOD = 3;
342 const REASON_BROKE_MY_SITE = 4;
343 const REASON_SUDDENLY_STOPPED_WORKING = 5;
344 const REASON_CANT_PAY_ANYMORE = 6;
345 const REASON_OTHER = 7;
346 const REASON_DIDNT_WORK = 8;
347 const REASON_DONT_LIKE_TO_SHARE_MY_INFORMATION = 9;
348 const REASON_COULDNT_MAKE_IT_WORK = 10;
349 const REASON_GREAT_BUT_NEED_SPECIFIC_FEATURE = 11;
350 const REASON_NOT_WORKING = 12;
351 const REASON_NOT_WHAT_I_WAS_LOOKING_FOR = 13;
352 const REASON_DIDNT_WORK_AS_EXPECTED = 14;
353 const REASON_TEMPORARY_DEACTIVATION = 15;
354
355 #endregion
356
357 /* Ctor
358 ------------------------------------------------------------------------------------------------------------------*/
359
360 /**
361 * Main singleton instance.
362 *
363 * @author Vova Feldman (@svovaf)
364 * @since 1.0.0
365 *
366 * @param number $module_id
367 * @param string|bool $slug
368 * @param bool $is_init Since 1.2.1 Is initiation sequence.
369 */
370 private function __construct( $module_id, $slug = false, $is_init = false ) {
371 if ( $is_init && is_numeric( $module_id ) && is_string( $slug ) ) {
372 $this->store_id_slug_type_path_map( $module_id, $slug );
373 }
374
375 $this->_module_id = $module_id;
376 $this->_slug = $this->get_slug();
377 $this->_module_type = $this->get_module_type();
378
379 $this->_blog_id = is_multisite() ? get_current_blog_id() : null;
380
381 $this->_storage = FS_Storage::instance( $this->_module_type, $this->_slug );
382
383 $this->_cache = FS_Cache_Manager::get_manager( WP_FS___OPTION_PREFIX . "cache_{$module_id}" );
384
385 $this->_logger = FS_Logger::get_logger( WP_FS__SLUG . '_' . $this->get_unique_affix(), WP_FS__DEBUG_SDK, WP_FS__ECHO_DEBUG_SDK );
386
387 $this->_plugin_main_file_path = $this->_find_caller_plugin_file( $is_init );
388 $this->_plugin_dir_path = plugin_dir_path( $this->_plugin_main_file_path );
389 $this->_plugin_basename = $this->get_plugin_basename();
390 $this->_free_plugin_basename = str_replace( '-premium/', '/', $this->_plugin_basename );
391
392 $this->_is_multisite_integrated = (
393 defined( "WP_FS__PRODUCT_{$module_id}_MULTISITE" ) &&
394 ( true === constant( "WP_FS__PRODUCT_{$module_id}_MULTISITE" ) )
395 );
396
397 $this->_is_network_active = (
398 is_multisite() &&
399 $this->_is_multisite_integrated &&
400 // Themes are always network activated, but the ACTUAL activation is per site.
401 $this->is_plugin() &&
402 (
403 is_plugin_active_for_network( $this->_plugin_basename ) ||
404 // Plugin network level activation or uninstall.
405 ( fs_is_network_admin() && is_plugin_inactive( $this->_plugin_basename ) )
406 )
407 );
408
409 $this->_storage->set_network_active(
410 $this->_is_network_active,
411 $this->is_delegated_connection()
412 );
413
414 if ( ! isset( $this->_storage->is_network_activated ) ) {
415 $this->_storage->is_network_activated = $this->_is_network_active;
416 }
417
418 if ( $this->_storage->is_network_activated != $this->_is_network_active ) {
419 // Update last activation level.
420 $this->_storage->is_network_activated = $this->_is_network_active;
421
422 $this->maybe_adjust_storage();
423 }
424
425 #region Migration
426
427 if ( is_multisite() ) {
428 /**
429 * If the install_timestamp exists on the site level but doesn't exist on the
430 * network level storage, it means that we need to process the storage with migration.
431 *
432 * 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.
433 *
434 * @author Vova Feldman (@svovaf)
435 * @since 2.0.0
436 */
437 if ( false === $this->_storage->get( 'install_timestamp', false, true ) &&
438 false !== $this->_storage->get( 'install_timestamp', false, false )
439 ) {
440 // Initiate storage migration.
441 $this->_storage->migrate_to_network();
442
443 // Migrate module cache to network level storage.
444 $this->_cache->migrate_to_network();
445 }
446 }
447
448 #endregion
449
450 $base_name_split = explode( '/', $this->_plugin_basename );
451 $this->_plugin_dir_name = $base_name_split[0];
452
453 if ( $this->_logger->is_on() ) {
454 $this->_logger->info( 'plugin_main_file_path = ' . $this->_plugin_main_file_path );
455 $this->_logger->info( 'plugin_dir_path = ' . $this->_plugin_dir_path );
456 $this->_logger->info( 'plugin_basename = ' . $this->_plugin_basename );
457 $this->_logger->info( 'free_plugin_basename = ' . $this->_free_plugin_basename );
458 $this->_logger->info( 'plugin_dir_name = ' . $this->_plugin_dir_name );
459 }
460
461 // Remember link between file to slug.
462 $this->store_file_slug_map();
463
464 // Store plugin's initial install timestamp.
465 if ( ! isset( $this->_storage->install_timestamp ) ) {
466 $this->_storage->install_timestamp = WP_FS__SCRIPT_START_TIME;
467 }
468
469 if ( ! is_object( $this->_plugin ) ) {
470 $this->_plugin = FS_Plugin_Manager::instance( $this->_module_id )->get();
471 }
472
473 $this->_admin_notices = FS_Admin_Notices::instance(
474 $this->_slug . ( $this->is_theme() ? ':theme' : '' ),
475 /**
476 * Ensure that the admin notice will always have a title by using the stored plugin title if available and
477 * retrieving the title via the "get_plugin_name" method if there is no stored plugin title available.
478 *
479 * @author Leo Fajardo (@leorw)
480 * @since 1.2.2
481 */
482 ( is_object( $this->_plugin ) ? $this->_plugin->title : $this->get_plugin_name() ),
483 $this->get_unique_affix()
484 );
485
486 if ( 'true' === fs_request_get( 'fs_clear_api_cache' ) ||
487 'true' === fs_request_is_action( 'restart_freemius' )
488 ) {
489 FS_Api::clear_cache();
490 $this->_cache->clear();
491 }
492
493 $this->_register_hooks();
494
495 /**
496 * Starting from version 2.0.0, `FS_Site` entities no longer have the `plan` property and have `plan_id`
497 * instead. This should be called before calling `_load_account()`, otherwise, `$this->_site` will not be
498 * loaded in `_load_account` for versions of SDK starting from 2.0.0.
499 *
500 * @author Leo Fajardo (@leorw)
501 */
502 self::migrate_install_plan_to_plan_id( $this->_storage );
503
504 $this->_load_account();
505
506 $this->_version_updates_handler();
507 }
508
509 /**
510 * @author Leo Fajardo (@leorw)
511 * @since 2.3.0
512 */
513 private function maybe_adjust_storage() {
514 $install_timestamp = null;
515 $prev_is_premium = null;
516
517 $options_to_update = array();
518
519 $is_network_admin = fs_is_network_admin();
520
521 $network_install_timestamp = $this->_storage->get( 'install_timestamp', null, true );
522
523 if ( ! $is_network_admin ) {
524 if ( is_null( $network_install_timestamp ) ) {
525 // Plugin was not network-activated before.
526 return;
527 }
528
529 if ( is_null( $this->_storage->get( 'install_timestamp', null, false ) ) ) {
530 // Set the `install_timestamp` only if it's not yet set.
531 $install_timestamp = $network_install_timestamp;
532 }
533
534 $prev_is_premium = $this->_storage->get( 'prev_is_premium', null, true );
535 } else {
536 $current_wp_user = self::_get_current_wp_user();
537 $current_fs_user = self::_get_user_by_email( $current_wp_user->user_email );
538 $network_user_info = array();
539
540 $skips_count = 0;
541
542 $sites = self::get_sites();
543 $sites_count = count( $sites );
544
545 $blog_id_2_install_map = array();
546
547 $is_first_non_ignored_blog = true;
548
549 foreach ( $sites as $site ) {
550 $blog_id = self::get_site_blog_id( $site );
551
552 $blog_install_timestamp = $this->_storage->get( 'install_timestamp', null, $blog_id );
553
554 if ( is_null( $blog_install_timestamp ) ) {
555 // Plugin has not been installed on this blog.
556 continue;
557 }
558
559 $is_earlier_install = (
560 ! is_null( $install_timestamp ) &&
561 $blog_install_timestamp < $install_timestamp
562 );
563
564 $install = $this->get_install_by_blog_id( $blog_id );
565
566 $update_network_user_info = false;
567
568 if ( ! is_object( $install ) ) {
569 if ( ! $this->_storage->get( 'is_anonymous', false, $blog_id ) ) {
570 // The opt-in decision (whether to skip or opt in) is yet to be made.
571 continue;
572 }
573
574 $skips_count ++;
575 } else {
576 $blog_id_2_install_map[ $blog_id ] = $install;
577
578 if ( empty( $network_user_info ) ) {
579 // Set the network user info for the 1st time. Choose any user information whether or not it is for the current WP user.
580 $update_network_user_info = true;
581 }
582
583 if ( ! $update_network_user_info &&
584 is_object( $current_fs_user ) &&
585 $network_user_info['user_id'] != $current_fs_user->id &&
586 $install->user_id == $current_fs_user->id
587 ) {
588 // If an install that is owned by the current WP user is found, use its user information instead.
589 $update_network_user_info = true;
590 }
591
592 if ( ! $update_network_user_info &&
593 $is_earlier_install &&
594 ( ! is_object( $current_fs_user ) || $current_fs_user->id == $install->user_id )
595 ) {
596 // 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.
597 $update_network_user_info = true;
598 }
599 }
600
601 if ( $update_network_user_info ) {
602 $network_user_info = array(
603 'user_id' => $install->user_id,
604 'blog_id' => $blog_id
605 );
606 }
607
608 $site_prev_is_premium = $this->_storage->get( 'prev_is_premium', null, $blog_id );
609
610 if ( $is_first_non_ignored_blog ) {
611 $prev_is_premium = $site_prev_is_premium;
612
613 if ( is_null( $network_install_timestamp ) ) {
614 $install_timestamp = $blog_install_timestamp;
615 }
616
617 $is_first_non_ignored_blog = false;
618
619 continue;
620 }
621
622 if ( ! is_null( $prev_is_premium ) && $prev_is_premium !== $site_prev_is_premium ) {
623 // If a different `$site_prev_is_premium` value is found, do not include the option in the collection of options to update.
624 $prev_is_premium = null;
625 }
626
627 if ( $is_earlier_install ) {
628 // If an earlier install timestamp is found.
629 $install_timestamp = $blog_install_timestamp;
630 }
631 }
632
633 $installs_count = count( $blog_id_2_install_map );
634
635 if ( $sites_count === ( $installs_count + $skips_count ) ) {
636 if ( ! empty( $network_user_info ) ) {
637 $options_to_update['network_user_id'] = $network_user_info['user_id'];
638 $options_to_update['network_install_blog_id'] = $network_user_info['blog_id'];
639
640 foreach ( $blog_id_2_install_map as $blog_id => $install ) {
641 if ( $install->user_id == $network_user_info['user_id'] ) {
642 continue;
643 }
644
645 $this->_storage->store( 'is_delegated_connection', true, $blog_id );
646 }
647 }
648
649 if ( $sites_count === $skips_count ) {
650 /**
651 * Assume network-level skipping as the intended action if all actions identified were only
652 * skipping of the connection (i.e., no opt-ins and delegated connections so far).
653 */
654 $options_to_update['is_anonymous_ms'] = true;
655 } else if ( $sites_count === $installs_count ) {
656 /**
657 * Assume network-level opt-in as the intended action if all actions identified were only opt-ins
658 * (i.e., no delegation and skipping of the connections so far).
659 */
660 $options_to_update['is_network_connected'] = true;
661 }
662 }
663 }
664
665 if ( ! is_null( $install_timestamp ) ) {
666 $options_to_update['install_timestamp'] = $install_timestamp;
667 }
668
669 if ( ! is_null( $prev_is_premium ) ) {
670 $options_to_update['prev_is_premium'] = $prev_is_premium;
671 }
672
673 if ( ! empty( $options_to_update ) ) {
674 $this->adjust_storage( $options_to_update, $is_network_admin );
675 }
676 }
677
678 /**
679 * @author Leo Fajardo (@leorw)
680 * @since 2.3.0
681 *
682 * @param array $options
683 * @param bool $is_network_admin
684 */
685 private function adjust_storage( $options, $is_network_admin ) {
686 foreach ( $options as $name => $value ) {
687 $this->_storage->store( $name, $value, $is_network_admin ? true : null );
688 }
689 }
690
691 /**
692 * Checks whether this module has a settings menu.
693 *
694 * @author Leo Fajardo (@leorw)
695 * @since 1.2.2
696 *
697 * @return bool
698 */
699 function has_settings_menu() {
700 return ( $this->_is_network_active && fs_is_network_admin() ) ?
701 $this->_menu->has_network_menu() :
702 $this->_menu->has_menu();
703 }
704
705 /**
706 * Check if the context module is free wp.org theme.
707 *
708 * This method is helpful because:
709 * 1. wp.org themes are limited to a single submenu item,
710 * and sub-submenu items are most likely not allowed (never verified).
711 * 2. wp.org themes are not allowed to redirect the user
712 * after the theme activation, therefore, the agreed UX
713 * is showing the opt-in as a modal dialog box after
714 * activation (approved by @otto42, @emiluzelac, @greenshady, @grapplerulrich).
715 *
716 * @author Vova Feldman (@svovaf)
717 * @since 1.2.2.7
718 *
719 * @return bool
720 */
721 function is_free_wp_org_theme() {
722 return (
723 $this->is_theme() &&
724 $this->is_org_repo_compliant() &&
725 ! $this->is_premium()
726 );
727 }
728
729 /**
730 * Checks whether this a submenu item is visible.
731 *
732 * @author Vova Feldman (@svovaf)
733 * @since 1.2.2.6
734 * @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.
735 *
736 * @param string $slug
737 * @param bool $ignore_free_wp_org_theme_context This is used to decide if the associated tab should be shown
738 * or hidden.
739 *
740 * @return bool
741 */
742 function is_submenu_item_visible( $slug, $ignore_free_wp_org_theme_context = false ) {
743 if ( $this->is_admin_page( $slug ) ) {
744 /**
745 * It is the current context page, so show the submenu item
746 * so the user will have the right context page, even if it
747 * was set to hidden.
748 */
749 return true;
750 }
751
752 if ( ! $this->has_settings_menu() ) {
753 // No menu settings at all.
754 return false;
755 }
756
757 if ( ! $ignore_free_wp_org_theme_context && $this->is_free_wp_org_theme() ) {
758 /**
759 * wp.org themes are limited to a single submenu item, and
760 * sub-submenu items are most likely not allowed (never verified).
761 */
762 return false;
763 }
764
765 return $this->_menu->is_submenu_item_visible( $slug );
766 }
767
768 /**
769 * Check if a Freemius page should be accessible via the UI.
770 *
771 * @author Vova Feldman (@svovaf)
772 * @since 1.2.2.7
773 *
774 * @param string $slug
775 *
776 * @return bool
777 */
778 function is_page_visible( $slug ) {
779 if ( $this->is_admin_page( $slug ) ) {
780 return true;
781 }
782
783 return $this->_menu->is_submenu_item_visible( $slug, true, true );
784 }
785
786 /**
787 * @author Vova Feldman (@svovaf)
788 * @since 1.0.9
789 */
790 private function _version_updates_handler() {
791 if ( ! isset( $this->_storage->sdk_version ) || $this->_storage->sdk_version != $this->version ) {
792 // Freemius version upgrade mode.
793 $this->_storage->sdk_last_version = $this->_storage->sdk_version;
794 $this->_storage->sdk_version = $this->version;
795
796 if ( empty( $this->_storage->sdk_last_version ) ||
797 version_compare( $this->_storage->sdk_last_version, $this->version, '<' )
798 ) {
799 $this->_storage->sdk_upgrade_mode = true;
800 $this->_storage->sdk_downgrade_mode = false;
801 } else {
802 $this->_storage->sdk_downgrade_mode = true;
803 $this->_storage->sdk_upgrade_mode = false;
804
805 }
806
807 $this->do_action( 'sdk_version_update', $this->_storage->sdk_last_version, $this->version );
808 }
809
810 $plugin_version = $this->get_plugin_version();
811 if ( ! isset( $this->_storage->plugin_version ) || $this->_storage->plugin_version != $plugin_version ) {
812 // Plugin version upgrade mode.
813 $this->_storage->plugin_last_version = $this->_storage->plugin_version;
814 $this->_storage->plugin_version = $plugin_version;
815
816 if ( empty( $this->_storage->plugin_last_version ) ||
817 version_compare( $this->_storage->plugin_last_version, $plugin_version, '<' )
818 ) {
819 $this->_storage->plugin_upgrade_mode = true;
820 $this->_storage->plugin_downgrade_mode = false;
821 } else {
822 $this->_storage->plugin_downgrade_mode = true;
823 $this->_storage->plugin_upgrade_mode = false;
824 }
825
826 if ( ! empty( $this->_storage->plugin_last_version ) ) {
827 // Different version of the plugin was installed before, therefore it's an update.
828 $this->_storage->is_plugin_new_install = false;
829 }
830
831 $this->do_action( 'plugin_version_update', $this->_storage->plugin_last_version, $plugin_version );
832 }
833 }
834
835 #--------------------------------------------------------------------------------
836 #region Data Migration on SDK Update
837 #--------------------------------------------------------------------------------
838
839 /**
840 * @author Vova Feldman (@svovaf)
841 * @since 1.1.5
842 *
843 * @param string $sdk_prev_version
844 * @param string $sdk_version
845 */
846 function _sdk_version_update( $sdk_prev_version, $sdk_version ) {
847 /**
848 * @since 1.1.7.3 Fixed unwanted connectivity test cleanup.
849 */
850 if ( empty( $sdk_prev_version ) ) {
851 return;
852 }
853
854 if ( version_compare( $sdk_prev_version, '2.1.0', '<' ) &&
855 version_compare( $sdk_version, '2.1.0', '>=' )
856 ) {
857 $this->_storage->handle_gdpr_admin_notice = true;
858 }
859
860 if ( version_compare( $sdk_prev_version, '2.0.0', '<' ) &&
861 version_compare( $sdk_version, '2.0.0', '>=' )
862 ) {
863 $this->migrate_to_subscriptions_collection();
864
865 $this->consolidate_licenses();
866
867 // Clear trial_plan since it's now loaded from the plans collection when needed.
868 $this->_storage->remove( 'trial_plan', true, false );
869 }
870
871 if ( version_compare( $sdk_prev_version, '1.2.3', '<' ) &&
872 version_compare( $sdk_version, '1.2.3', '>=' )
873 ) {
874 /**
875 * Starting from version 1.2.3, paths are stored as relative instead of absolute and some of them can be
876 * invalid.
877 *
878 * @author Leo Fajardo (@leorw)
879 */
880 $this->remove_invalid_paths();
881 }
882
883 if ( version_compare( $sdk_prev_version, '1.1.5', '<' ) &&
884 version_compare( $sdk_version, '1.1.5', '>=' )
885 ) {
886 // On version 1.1.5 merged connectivity and is_on data.
887 if ( isset( $this->_storage->connectivity_test ) ) {
888 if ( ! isset( $this->_storage->is_on ) ) {
889 unset( $this->_storage->connectivity_test );
890 } else {
891 $connectivity_data = $this->_storage->connectivity_test;
892 $connectivity_data['is_active'] = $this->_storage->is_on['is_active'];
893 $connectivity_data['timestamp'] = $this->_storage->is_on['timestamp'];
894
895 // Override.
896 $this->_storage->connectivity_test = $connectivity_data;
897
898 // Remove previous structure.
899 unset( $this->_storage->is_on );
900 }
901
902 }
903 }
904
905 if (
906 version_compare( $sdk_prev_version, '2.2.1', '<' ) &&
907 version_compare( $sdk_version, '2.2.1', '>=' )
908 ) {
909 /**
910 * Clear the file cache without storing the previous path since it could be a wrong path. For example,
911 * in the versions of the SDK lower than 2.2.1, it's possible for the path of an add-on to be the same
912 * as the parent plugin's when the add-on was auto-installed since the relevant method names were not
913 * skipped in the logic that determines the right path in the `get_caller_main_file_and_type` method
914 * (e.g. `try_activate_plugin`). Since it was an auto-installation, the caller was the parent plugin
915 * and so its path was used. In case the stored path is wrong, clearing the cache will resolve issues
916 * related to data mix-up between plugins (e.g. titles and versions of an add-on and its parent plugin).
917 *
918 * @author Leo Fajardo (@leorw)
919 * @since 2.2.1
920 */
921 $this->clear_module_main_file_cache( false );
922 }
923 }
924
925 /**
926 * @author Leo Fajardo (@leorw)
927 * @since 2.0.0
928 *
929 * @param \FS_Storage $storage
930 * @param bool|int|null $blog_id
931 */
932 private static function migrate_install_plan_to_plan_id( FS_Storage $storage, $blog_id = null ) {
933 if ( empty( $storage->sdk_version ) ) {
934 // New installation of the plugin, no need to upgrade.
935 return;
936 }
937
938 if ( ! version_compare( $storage->sdk_version, '2.0.0', '<' ) ) {
939 // Previous version is >= 2.0.0, so no need to migrate.
940 return;
941 }
942
943 // Alias.
944 $module_type = $storage->get_module_type();
945 $module_slug = $storage->get_module_slug();
946
947 $installs = self::get_all_sites( $module_type, $blog_id );
948 $install = isset( $installs[ $module_slug ] ) ? $installs[ $module_slug ] : null;
949
950 if ( ! is_object( $install ) ) {
951 return;
952 }
953
954 if ( isset( $install->plan ) && is_object( $install->plan ) ) {
955 if ( isset( $install->plan->id ) && ! empty( $install->plan->id ) ) {
956 $install->plan_id = self::_decrypt( $install->plan->id );
957 }
958
959 unset( $install->plan );
960
961 $installs[ $module_slug ] = clone $install;
962
963 self::set_account_option_by_module(
964 $module_type,
965 'sites',
966 $installs,
967 true,
968 $blog_id
969 );
970 }
971 }
972
973 /**
974 * @author Leo Fajardo (@leorw)
975 * @since 2.0.0
976 */
977 private function migrate_to_subscriptions_collection() {
978 if ( ! is_object( $this->_site ) ) {
979 return;
980 }
981
982 if ( isset( $this->_storage->subscription ) && is_object( $this->_storage->subscription ) ) {
983 $this->_storage->subscriptions = array( $this->_storage->subscription );
984 }
985 }
986
987 /**
988 * @author Leo Fajardo (@leorw)
989 * @since 2.0.0
990 */
991 private function consolidate_licenses() {
992 $plugin_licenses = self::get_account_option( 'licenses', WP_FS__MODULE_TYPE_PLUGIN );
993 if ( isset( $plugin_licenses[ $this->_slug ] ) ) {
994 $plugin_licenses = $plugin_licenses[ $this->_slug ];
995 } else {
996 $plugin_licenses = array();
997 }
998
999 $theme_licenses = self::get_account_option( 'licenses', WP_FS__MODULE_TYPE_THEME );
1000 if ( isset( $theme_licenses[ $this->_slug ] ) ) {
1001 $theme_licenses = $theme_licenses[ $this->_slug ];
1002 } else {
1003 $theme_licenses = array();
1004 }
1005
1006 if ( empty( $plugin_licenses ) && empty( $theme_licenses ) ) {
1007 return;
1008 }
1009
1010 $all_licenses = array();
1011 $user_id_license_ids_map = array();
1012
1013 foreach ( $plugin_licenses as $user_id => $user_licenses ) {
1014 if ( is_array( $user_licenses ) ) {
1015 if ( ! isset( $user_license_ids[ $user_id ] ) ) {
1016 $user_id_license_ids_map[ $user_id ] = array();
1017 }
1018
1019 foreach ( $user_licenses as $user_license ) {
1020 $all_licenses[] = $user_license;
1021 $user_id_license_ids_map[ $user_id ][] = $user_license->id;
1022 }
1023 }
1024 }
1025
1026 foreach ( $theme_licenses as $user_id => $user_licenses ) {
1027 if ( is_array( $user_licenses ) ) {
1028 if ( ! isset( $user_license_ids[ $user_id ] ) ) {
1029 $user_id_license_ids_map[ $user_id ] = array();
1030 }
1031
1032 foreach ( $user_licenses as $user_license ) {
1033 $all_licenses[] = $user_license;
1034 $user_id_license_ids_map[ $user_id ][] = $user_license->id;
1035 }
1036 }
1037 }
1038
1039 self::store_user_id_license_ids_map(
1040 $user_id_license_ids_map,
1041 $this->_module_id
1042 );
1043
1044 $this->_store_licenses( true, $this->_module_id, $all_licenses );
1045 }
1046
1047 /**
1048 * Remove invalid paths.
1049 *
1050 * @author Leo Fajardo (@leorw)
1051 * @since 1.2.3
1052 */
1053 private function remove_invalid_paths() {
1054 // Remove invalid path that is still associated with the current slug if there's any.
1055 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
1056 foreach ( $file_slug_map as $plugin_basename => $slug ) {
1057 if ( $slug === $this->_slug &&
1058 $plugin_basename !== $this->_plugin_basename &&
1059 ! file_exists( $this->get_absolute_path( $plugin_basename ) )
1060 ) {
1061 unset( $file_slug_map[ $plugin_basename ] );
1062 self::$_accounts->set_option( 'file_slug_map', $file_slug_map, true );
1063
1064 break;
1065 }
1066 }
1067 }
1068
1069 /**
1070 * @author Vova Feldman (@svovaf)
1071 * @since 1.2.2.7
1072 *
1073 * @param string $plugin_prev_version
1074 * @param string $plugin_version
1075 */
1076 function _after_version_update( $plugin_prev_version, $plugin_version ) {
1077 if ( $this->is_theme() ) {
1078 // Expire the cache of the previous tabs since the theme may
1079 // have setting updates.
1080 $this->_cache->expire( 'tabs' );
1081 $this->_cache->expire( 'tabs_stylesheets' );
1082 }
1083 }
1084
1085 /**
1086 * A special migration logic for the $_accounts, executed for all the plugins in the system:
1087 * - Moves some data to the network level storage.
1088 * - If the plugin's connection was skipped for all sites, set the plugin as if it was network skipped.
1089 * - If the plugin's connection was ignored for all sites, don't do anything in terms of the network connection.
1090 * - 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.
1091 * - 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.
1092 * - 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.
1093 *
1094 * @author Vova Feldman (@svovaf)
1095 * @since 2.0.0
1096 */
1097 private static function migrate_accounts_to_network() {
1098 $sites = self::get_sites();
1099 $sites_count = count( $sites );
1100 $connection_status = array();
1101 $plugin_slugs = array();
1102 foreach ( $sites as $site ) {
1103 $blog_id = self::get_site_blog_id( $site );
1104
1105 self::$_accounts->migrate_to_network( $blog_id );
1106
1107 /**
1108 * Build a list of all Freemius powered plugins slugs.
1109 */
1110 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array(), $blog_id );
1111 foreach ( $id_slug_type_path_map as $module_id => $data ) {
1112 if ( WP_FS__MODULE_TYPE_PLUGIN === $data['type'] ) {
1113 $plugin_slugs[ $data['slug'] ] = true;
1114 }
1115 }
1116
1117 $installs = self::get_account_option( 'sites', WP_FS__MODULE_TYPE_PLUGIN, $blog_id );
1118
1119 if ( is_array( $installs ) ) {
1120 foreach ( $installs as $slug => $install ) {
1121 if ( ! isset( $connection_status[ $slug ] ) ) {
1122 $connection_status[ $slug ] = array();
1123 }
1124
1125 if ( is_object( $install ) &&
1126 FS_Site::is_valid_id( $install->id ) &&
1127 FS_User::is_valid_id( $install->user_id )
1128 ) {
1129 $connection_status[ $slug ][ $blog_id ] = $install->user_id;
1130 }
1131 }
1132 }
1133 }
1134
1135 foreach ( $plugin_slugs as $slug => $true ) {
1136 if ( ! isset( $connection_status[ $slug ] ) ) {
1137 $connection_status[ $slug ] = array();
1138 }
1139
1140 foreach ( $sites as $site ) {
1141 $blog_id = self::get_site_blog_id( $site );
1142
1143 if ( isset( $connection_status[ $slug ][ $blog_id ] ) ) {
1144 continue;
1145 }
1146
1147 $storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
1148
1149 $is_anonymous = $storage->get( 'is_anonymous', null, $blog_id );
1150
1151 if ( ! is_null( $is_anonymous ) ) {
1152 // Since 1.1.3 is_anonymous is an array.
1153 if ( is_array( $is_anonymous ) && isset( $is_anonymous['is'] ) ) {
1154 $is_anonymous = $is_anonymous['is'];
1155 }
1156
1157 if ( is_bool( $is_anonymous ) && true === $is_anonymous ) {
1158 $connection_status[ $slug ][ $blog_id ] = 'skipped';
1159 }
1160 }
1161
1162 if ( ! isset( $connection_status[ $slug ][ $blog_id ] ) ) {
1163 $connection_status[ $slug ][ $blog_id ] = 'ignored';
1164 }
1165 }
1166 }
1167
1168 $super_admins = array();
1169
1170 foreach ( $connection_status as $slug => $blogs_status ) {
1171 $skips = 0;
1172 $ignores = 0;
1173 $connections = 0;
1174 $opted_in_users = array();
1175 $opted_in_super_admins = array();
1176
1177 $storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
1178
1179 foreach ( $blogs_status as $blog_id => $status_or_user_id ) {
1180 if ( 'skipped' === $status_or_user_id ) {
1181 $skips ++;
1182 } else if ( 'ignored' === $status_or_user_id ) {
1183 $ignores ++;
1184 } else if ( FS_User::is_valid_id( $status_or_user_id ) ) {
1185 $connections ++;
1186
1187 if ( ! isset( $opted_in_users[ $status_or_user_id ] ) ) {
1188 $opted_in_users[ $status_or_user_id ] = array();
1189 }
1190
1191 $opted_in_users[ $status_or_user_id ][] = $blog_id;
1192
1193 if ( isset( $super_admins[ $status_or_user_id ] ) ||
1194 self::is_super_admin( $status_or_user_id )
1195 ) {
1196 // Cache super-admin data.
1197 $super_admins[ $status_or_user_id ] = true;
1198
1199 // Remember opted-in super-admins for the plugin.
1200 $opted_in_super_admins[ $status_or_user_id ] = true;
1201 }
1202 }
1203 }
1204
1205 $main_super_admin_user_id = null;
1206 $all_migrated = false;
1207 if ( $sites_count == $skips ) {
1208 // All sites were skipped -> network skip by copying the anonymous mode from any of the sites.
1209 $storage->is_anonymous_ms = $storage->is_anonymous;
1210
1211 $all_migrated = true;
1212 } else if ( $sites_count == $ignores ) {
1213 // Don't do anything, still in activation mode.
1214
1215 $all_migrated = true;
1216 } else if ( 0 < count( $opted_in_super_admins ) ) {
1217 // Find the super-admin with the majority of installs.
1218 $max_installs_by_super_admin = 0;
1219 foreach ( $opted_in_super_admins as $user_id => $true ) {
1220 $installs_count = count( $opted_in_users[ $user_id ] );
1221
1222 if ( $installs_count > $max_installs_by_super_admin ) {
1223 $max_installs_by_super_admin = $installs_count;
1224 $main_super_admin_user_id = $user_id;
1225 }
1226 }
1227
1228 if ( $sites_count == $connections && 1 == count( $opted_in_super_admins ) ) {
1229 // Super-admin opted-in for all sites in the network.
1230 $storage->is_network_connected = true;
1231
1232 $all_migrated = true;
1233 }
1234
1235 // Store network user.
1236 $storage->network_user_id = $main_super_admin_user_id;
1237
1238 $storage->network_install_blog_id = ( $sites_count == $connections ) ?
1239 // Since all sites are opted-in, associating with the main site.
1240 get_current_blog_id() :
1241 // Associating with the 1st found opted-in site.
1242 $opted_in_users[ $main_super_admin_user_id ][0];
1243
1244 /**
1245 * Make sure we migrate the plan ID of the network install, otherwise, if after the migration
1246 * the 1st page that will be loaded is the network level WP Admin and $storage->network_install_blog_id
1247 * is different than the main site of the network, the $this->_site will not be set since the plan_id
1248 * will be empty.
1249 */
1250 $storage->migrate_to_network();
1251 self::migrate_install_plan_to_plan_id( $storage, $storage->network_install_blog_id );
1252 } else {
1253 // At least one opt-in. All the opt-in were created by a non-super-admin.
1254 if ( 0 == $ignores ) {
1255 // All sites were opted-in or skipped, all by non-super-admin. So delegate all.
1256 $storage->store( 'is_delegated_connection', true, true );
1257
1258 $all_migrated = true;
1259 }
1260 }
1261
1262 if ( ! $all_migrated ) {
1263 /**
1264 * Delegate all sites that were:
1265 * 1) Opted-in by a user that is NOT the main-super-admin.
1266 * 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.
1267 */
1268 foreach ( $blogs_status as $blog_id => $status_or_user_id ) {
1269 if ( $status_or_user_id == $main_super_admin_user_id ) {
1270 continue;
1271 }
1272
1273 if ( FS_User::is_valid_id( $status_or_user_id ) ||
1274 ( 'skipped' === $status_or_user_id && is_null( $main_super_admin_user_id ) )
1275 ) {
1276 $storage->store( 'is_delegated_connection', true, $blog_id );
1277 }
1278 }
1279 }
1280
1281
1282 if ( ( $connections + $skips > 0 ) ) {
1283 if ( $ignores > 0 ) {
1284 /**
1285 * If admin already opted-in or skipped in any of the network sites, and also
1286 * have sites which the connection decision was not yet taken, set this plugin
1287 * into network activation mode so the super-admin can choose what to do with
1288 * the rest of the sites.
1289 */
1290 self::set_network_upgrade_mode( $storage );
1291 }
1292 }
1293 }
1294 }
1295
1296 /**
1297 * Set a module into network upgrade mode.
1298 *
1299 * @author Vova Feldman (@svovaf)
1300 * @since 2.0.0
1301 *
1302 * @param \FS_Storage $storage
1303 *
1304 * @return bool
1305 */
1306 private static function set_network_upgrade_mode( FS_Storage $storage ) {
1307 return $storage->is_network_activation = true;
1308 }
1309
1310 /**
1311 * Will return true after upgrading to the SDK with the network level integration,
1312 * when the super-admin involvement is required regarding the rest of the sites.
1313 *
1314 * @author Vova Feldman (@svovaf)
1315 * @since 2.0.0
1316 *
1317 * @return bool
1318 */
1319 function is_network_upgrade_mode() {
1320 return $this->_storage->get( 'is_network_activation' );
1321 }
1322
1323 /**
1324 * Clear flag after the upgrade mode completion.
1325 *
1326 * @author Vova Feldman (@svovaf)
1327 * @since 2.0.0
1328 *
1329 * @return bool True if network activation was on and now completed.
1330 */
1331 private function network_upgrade_mode_completed() {
1332 if ( fs_is_network_admin() && $this->is_network_upgrade_mode() ) {
1333 $this->_storage->remove( 'is_network_activation' );
1334
1335 return true;
1336 }
1337
1338 return false;
1339 }
1340
1341 #endregion
1342
1343 /**
1344 * This action is connected to the 'plugins_loaded' hook and helps to determine
1345 * if this is a new plugin installation or a plugin update.
1346 *
1347 * There are 3 different use-cases:
1348 * 1) New plugin installation right with Freemius:
1349 * 1.1 _activate_plugin_event_hook() will be executed first
1350 * 1.2 Since $this->_storage->is_plugin_new_install is not set,
1351 * and $this->_storage->plugin_last_version is not set,
1352 * $this->_storage->is_plugin_new_install will be set to TRUE.
1353 * 1.3 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install will
1354 * be already set to TRUE.
1355 *
1356 * 2) Plugin update, didn't have Freemius before, and now have the SDK:
1357 * 2.1 _activate_plugin_event_hook() will not be executed, because
1358 * the activation hook do NOT fires on updates since WP 3.1.
1359 * 2.2 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install will
1360 * be empty, therefore, it will be set to FALSE.
1361 *
1362 * 3) Plugin update, had Freemius in prev version as well:
1363 * 3.1 _version_updates_handler() will be executed 1st, since FS was installed
1364 * before, $this->_storage->plugin_last_version will NOT be empty,
1365 * therefore, $this->_storage->is_plugin_new_install will be set to FALSE.
1366 * 3.2 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install is
1367 * already set, therefore, it will not be modified.
1368 *
1369 * Use-case #3 is backward compatible, #3.1 will be executed since 1.0.9.
1370 *
1371 * NOTE:
1372 * The only fallback of this mechanism is if an admin updates a plugin based on use-case #2,
1373 * and then, the next immediate PageView is the plugin's main settings page, it will not
1374 * show the opt-in right away. The reason it will happen is because Freemius execution
1375 * will be turned off till the plugin is fully loaded at least once
1376 * (till $this->_storage->was_plugin_loaded is TRUE).
1377 *
1378 * @author Vova Feldman (@svovaf)
1379 * @since 1.1.9
1380 *
1381 */
1382 function _plugins_loaded() {
1383 // Update flag that plugin was loaded with Freemius at least once.
1384 $this->_storage->was_plugin_loaded = true;
1385
1386 /**
1387 * Bug fix - only set to false when it's a plugin, due to the
1388 * execution sequence of the theme hooks and our methods, if
1389 * this will be set for themes, Freemius will always assume
1390 * it's a theme update.
1391 *
1392 * @author Vova Feldman (@svovaf)
1393 * @since 1.2.2.2
1394 */
1395 if ( $this->is_plugin() &&
1396 ! isset( $this->_storage->is_plugin_new_install )
1397 ) {
1398 $this->_storage->is_plugin_new_install = false;
1399 }
1400 }
1401
1402 /**
1403 * Add special parameter to WP admin AJAX calls so when we
1404 * process AJAX calls we can identify its source properly.
1405 *
1406 * @author Leo Fajardo (@leorw)
1407 * @since 2.0.0
1408 */
1409 static function _enrich_ajax_url() {
1410 $admin_param = is_network_admin() ?
1411 '_fs_network_admin' :
1412 '_fs_blog_admin';
1413 ?>
1414 <script type="text/javascript">
1415 (function ($) {
1416 $(document).ajaxSend(function (event, jqxhr, settings) {
1417 if (settings.url &&
1418 -1 < settings.url.indexOf('admin-ajax.php') &&
1419 ! ( settings.url.indexOf( '<?php echo $admin_param ?>' ) > 0 )
1420 ) {
1421 if (settings.url.indexOf('?') > 0) {
1422 settings.url += '&';
1423 } else {
1424 settings.url += '?';
1425 }
1426
1427 settings.url += '<?php echo $admin_param ?>=true';
1428
1429 }
1430 });
1431 })(jQuery);
1432 </script>
1433 <?php
1434 }
1435
1436 /**
1437 * Opens the support forum subemenu item in a new browser page.
1438 *
1439 * @author Vova Feldman (@svovaf)
1440 * @since 2.1.4
1441 */
1442 static function _open_support_forum_in_new_page() {
1443 ?>
1444 <script type="text/javascript">
1445 (function ($) {
1446 $('.fs-submenu-item.wp-support-forum').parent().attr('target', '_blank');
1447 })(jQuery);
1448 </script>
1449 <?php
1450 }
1451
1452 /**
1453 * @author Vova Feldman (@svovaf)
1454 * @since 1.0.9
1455 */
1456 private function _register_hooks() {
1457 $this->_logger->entrance();
1458
1459 if ( is_admin() ) {
1460 add_action( 'plugins_loaded', array( &$this, '_hook_action_links_and_register_account_hooks' ) );
1461
1462 if ( $this->is_plugin() ) {
1463 if ( self::is_plugin_install_page() && true !== fs_request_get_bool( 'fs_allow_updater_and_dialog' ) ) {
1464 /**
1465 * Unless the `fs_allow_updater_and_dialog` URL param exists and its value is `true`, make
1466 * Freemius-related updates unavailable on the "Add Plugins" admin page (/plugin-install.php)
1467 * so that they won't interfere with the .org plugins' functionalities on that page (e.g.
1468 * updating of a .org plugin).
1469 */
1470 add_filter( 'site_transient_update_plugins', array( 'Freemius', '_remove_fs_updates_from_plugin_install_page' ), 10, 2 );
1471 } else if ( self::is_plugins_page() || self::is_updates_page() ) {
1472 /**
1473 * 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.
1474 *
1475 * @author Leo Fajardo (@leorw)
1476 * @since 2.2.3
1477 */
1478 add_action( 'admin_footer', array( 'Freemius', '_prepend_fs_allow_updater_and_dialog_flag_url_param' ) );
1479 }
1480
1481 $plugin_dir = dirname( $this->_plugin_dir_path ) . '/';
1482
1483 /**
1484 * @since 1.2.2
1485 *
1486 * Hook to both free and premium version activations to support
1487 * auto deactivation on the other version activation.
1488 */
1489 register_activation_hook(
1490 $plugin_dir . $this->_free_plugin_basename,
1491 array( &$this, '_activate_plugin_event_hook' )
1492 );
1493
1494 register_activation_hook(
1495 $plugin_dir . $this->premium_plugin_basename(),
1496 array( &$this, '_activate_plugin_event_hook' )
1497 );
1498 } else {
1499 add_action( 'after_switch_theme', array( &$this, '_activate_theme_event_hook' ), 10, 2 );
1500
1501 /**
1502 * Include the required hooks to capture the theme settings' page tabs
1503 * and cache them.
1504 *
1505 * @author Vova Feldman (@svovaf)
1506 * @since 1.2.2.7
1507 */
1508 if ( ! $this->_cache->has_valid( 'tabs' ) ) {
1509 add_action( 'admin_footer', array( &$this, '_tabs_capture' ) );
1510 // Add license activation AJAX callback.
1511 $this->add_ajax_action( 'store_tabs', array( &$this, '_store_tabs_ajax_action' ) );
1512
1513 add_action( 'admin_enqueue_scripts', array( &$this, '_store_tabs_styles' ), 9999999 );
1514 }
1515
1516 add_action(
1517 'admin_footer',
1518 array( &$this, '_add_freemius_tabs' ),
1519 /**
1520 * The tabs JS code must be executed after the tabs capture logic (_tabs_capture()).
1521 * That's why the priority is 11 while the tabs capture logic is added
1522 * with priority 10.
1523 *
1524 * @author Vova Feldman (@svovaf)
1525 */
1526 11
1527 );
1528
1529 add_action( 'admin_footer', array( &$this, '_style_premium_theme' ) );
1530 }
1531
1532 /**
1533 * Part of the mechanism to identify new plugin install vs. plugin update.
1534 *
1535 * @author Vova Feldman (@svovaf)
1536 * @since 1.1.9
1537 */
1538 if ( empty( $this->_storage->was_plugin_loaded ) ) {
1539 /**
1540 * During the plugin activation (not theme), 'plugins_loaded' will be already executed
1541 * when the logic gets here since the activation logic first add the activate plugins,
1542 * then triggers 'plugins_loaded', and only then include the code of the plugin that
1543 * is activated. Which means that _plugins_loaded() will NOT be executed during the
1544 * plugin activation, and that IS intentional.
1545 *
1546 * @author Vova Feldman (@svovaf)
1547 */
1548 if ( $this->is_plugin() && $this->is_activation_mode( false ) ) {
1549 add_action( 'plugins_loaded', array( &$this, '_plugins_loaded' ) );
1550 } else {
1551 // If was activated before, then it was already loaded before.
1552 $this->_plugins_loaded();
1553 }
1554 }
1555
1556 if ( ! self::is_ajax() ) {
1557 if ( ! $this->is_addon() ) {
1558 add_action( 'init', array( &$this, '_add_default_submenu_items' ), WP_FS__LOWEST_PRIORITY );
1559 }
1560 }
1561
1562 if ( $this->_storage->handle_gdpr_admin_notice ) {
1563 add_action( 'init', array( &$this, '_maybe_show_gdpr_admin_notice' ) );
1564 }
1565
1566 add_action( 'init', array( &$this, '_maybe_add_gdpr_optin_ajax_handler') );
1567 }
1568
1569 if ( $this->is_plugin() ) {
1570 if ( $this->_is_network_active ) {
1571 add_action( 'wpmu_new_blog', array( $this, '_after_new_blog_callback' ), 10, 6 );
1572 }
1573
1574 register_deactivation_hook( $this->_plugin_main_file_path, array( &$this, '_deactivate_plugin_hook' ) );
1575 }
1576
1577 if ( is_multisite() ) {
1578 add_action( 'deactivate_blog', array( &$this, '_after_site_deactivated_callback' ) );
1579 add_action( 'archive_blog', array( &$this, '_after_site_deactivated_callback' ) );
1580 add_action( 'make_spam_blog', array( &$this, '_after_site_deactivated_callback' ) );
1581 add_action( 'deleted_blog', array( &$this, '_after_site_deleted_callback' ), 10, 2 );
1582
1583 add_action( 'activate_blog', array( &$this, '_after_site_reactivated_callback' ) );
1584 add_action( 'unarchive_blog', array( &$this, '_after_site_reactivated_callback' ) );
1585 add_action( 'make_ham_blog', array( &$this, '_after_site_reactivated_callback' ) );
1586 }
1587
1588 if ( $this->is_theme() &&
1589 self::is_customizer() &&
1590 $this->apply_filters( 'show_customizer_upsell', true )
1591 ) {
1592 // Register customizer upsell.
1593 add_action( 'customize_register', array( &$this, '_customizer_register' ) );
1594 }
1595
1596 add_action( 'admin_init', array( &$this, '_redirect_on_clicked_menu_link' ), WP_FS__LOWEST_PRIORITY );
1597
1598 if ( $this->is_theme() ) {
1599 add_action( 'admin_init', array( &$this, '_add_tracking_links' ) );
1600 }
1601
1602 add_action( 'admin_init', array( &$this, '_add_license_activation' ) );
1603 add_action( 'admin_init', array( &$this, '_add_premium_version_upgrade_selection' ) );
1604 add_action( 'admin_init', array( &$this, '_add_beta_mode_update_handler' ) );
1605
1606 $this->add_ajax_action( 'update_billing', array( &$this, '_update_billing_ajax_action' ) );
1607 $this->add_ajax_action( 'start_trial', array( &$this, '_start_trial_ajax_action' ) );
1608
1609 if ( $this->_is_network_active && fs_is_network_admin() ) {
1610 $this->add_ajax_action( 'network_activate', array( &$this, '_network_activate_ajax_action' ) );
1611 }
1612
1613 $this->add_ajax_action( 'install_premium_version', array(
1614 &$this,
1615 '_install_premium_version_ajax_action'
1616 ) );
1617
1618 $this->add_ajax_action( 'submit_affiliate_application', array( &$this, '_submit_affiliate_application' ) );
1619
1620 $this->add_action( 'after_plans_sync', array( &$this, '_check_for_trial_plans' ) );
1621
1622 $this->add_action( 'sdk_version_update', array( &$this, '_sdk_version_update' ), WP_FS__DEFAULT_PRIORITY, 2 );
1623
1624 $this->add_action(
1625 'plugin_version_update',
1626 array( &$this, '_after_version_update' ),
1627 WP_FS__DEFAULT_PRIORITY,
1628 2
1629 );
1630 $this->add_filter( 'after_code_type_change', array( &$this, '_after_code_type_change' ) );
1631
1632 add_action( 'admin_init', array( &$this, '_add_trial_notice' ) );
1633 add_action( 'admin_init', array( &$this, '_add_affiliate_program_notice' ) );
1634 add_action( 'admin_enqueue_scripts', array( &$this, '_enqueue_common_css' ) );
1635
1636 /**
1637 * Handle request to reset anonymous mode for `get_reconnect_url()`.
1638 *
1639 * @author Vova Feldman (@svovaf)
1640 * @since 1.2.1.5
1641 */
1642 if ( fs_request_is_action( 'reset_anonymous_mode' ) &&
1643 $this->get_unique_affix() === fs_request_get( 'fs_unique_affix' )
1644 ) {
1645 add_action( 'admin_init', array( &$this, 'connect_again' ) );
1646 }
1647 }
1648
1649 /**
1650 * Makes Freemius-related updates unavailable on the "Add Plugins" admin page (/plugin-install.php) so that
1651 * they won't interfere with the .org plugins' functionalities on that page (e.g. updating of a .org plugin).
1652 *
1653 * @author Leo Fajardo (@leorw)
1654 * @since 2.2.3
1655 *
1656 * @param object $updates
1657 * @param string|null $transient
1658 *
1659 * @return object
1660 */
1661 static function _remove_fs_updates_from_plugin_install_page( $updates, $transient = null ) {
1662 if ( is_object( $updates ) && isset( $updates->response ) ) {
1663 foreach ( $updates->response as $file => $plugin ) {
1664 if ( isset( $plugin->package ) && false !== strpos( $plugin->package, 'api.freemius' ) ) {
1665 unset( $updates->response[ $file ] );
1666 }
1667 }
1668 }
1669
1670 return $updates;
1671 }
1672
1673 /**
1674 * Prepends the `fs_allow_updater_and_dialog` param to the plugin information URLs to tell the SDK to handle
1675 * the information that is shown on the plugin details dialog that is shown when the relevant link is clicked.
1676 *
1677 * @author Leo Fajardo (@leorw)
1678 * @since 2.2.3
1679 *
1680 * @return string
1681 */
1682 static function _prepend_fs_allow_updater_and_dialog_flag_url_param() {
1683 $slug_basename_map = array();
1684 foreach ( self::$_instances as $instance ) {
1685 if ( ! $instance->is_plugin() ) {
1686 continue;
1687 }
1688
1689 $slug_basename_map[ $instance->get_slug() ] = $instance->premium_plugin_basename();
1690 }
1691 ?>
1692 <script type="text/javascript">
1693 (function( $ ) {
1694 var slugBasenameMap = <?php echo json_encode( $slug_basename_map ) ?>;
1695 for ( var slug in slugBasenameMap ) {
1696 var basename = slugBasenameMap[ slug ];
1697
1698 // Try to get the plugin rows if on the "Plugins" page.
1699 var $pluginRows = $( '.wp-list-table.plugins tr[data-plugin="' + basename + '"]');
1700
1701 if ( 0 === $pluginRows.length ) {
1702 // Try to get the plugin rows if on the "Updates" page.
1703 var $pluginCheckbox = $( '#update-plugins-table input[type="checkbox"][value="' + basename + '"]' );
1704 if ( 0 !== $pluginCheckbox.length ) {
1705 $pluginRows = $pluginCheckbox.parents( 'tr:first' );
1706 }
1707 }
1708
1709 if ( 0 === $pluginRows.length ) {
1710 // No plugin rows found.
1711 continue;
1712 }
1713
1714 // Find the "View details" links and add the `fs_allow_updater_and_dialog` param to the URL.
1715 $pluginRows.find( 'a[href*="plugin-install.php?tab=plugin-information"]' ).each(function() {
1716 var $this = $( this ),
1717 href = $this.attr( 'href' ).replace( '?tab=', '?fs_allow_updater_and_dialog=true&tab=');
1718
1719 $this.attr( 'href', href );
1720 });
1721 }
1722 })( jQuery );
1723 </script>
1724 <?php
1725 }
1726
1727 /**
1728 * @author Leo Fajardo (@leorw)
1729 * @since 2.3.0
1730 */
1731 static function _maybe_add_beta_label_styles() {
1732 $has_any_beta_version = false;
1733
1734 foreach ( self::$_instances as $instance ) {
1735 if ( $instance->is_beta() ) {
1736 $has_any_beta_version = true;
1737 break;
1738 }
1739 }
1740
1741 if ( $has_any_beta_version ) {
1742 fs_enqueue_local_style( 'fs_plugins', '/admin/plugins.css' );
1743 }
1744 }
1745
1746 /**
1747 * @author Leo Fajardo (@leorw)
1748 * @since 2.3.0
1749 */
1750 static function _maybe_add_beta_label_to_plugins_and_handle_confirmation() {
1751 $beta_data = array();
1752
1753 foreach ( self::$_instances as $instance ) {
1754 if ( ! $instance->is_premium() ) {
1755 continue;
1756 }
1757
1758 /**
1759 * If there's an available beta version update, a confirmation message will be shown when the
1760 * "Update now" link on the "Plugins" or "Themes" page is clicked.
1761 */
1762 $has_beta_update = $instance->has_beta_update();
1763
1764 $is_beta = (
1765 // The "Beta" label is added separately for themes.
1766 $instance->is_plugin() &&
1767 $instance->is_beta()
1768 );
1769
1770 if ( ! $is_beta && ! $has_beta_update ) {
1771 continue;
1772 }
1773
1774 $beta_data[ $instance->get_plugin_basename() ] = array( 'is_installed_version_beta' => $is_beta );
1775
1776 if ( ! $has_beta_update ) {
1777 continue;
1778 }
1779
1780 $beta_data[ $instance->get_plugin_basename() ]['beta_version_update_confirmation_message'] = sprintf(
1781 '%s %s',
1782 sprintf(
1783 fs_esc_attr_inline(
1784 '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.',
1785 'beta-version-update-caution',
1786 $instance->get_slug()
1787 ),
1788 $instance->get_plugin_title()
1789 ),
1790 fs_esc_attr_inline( 'Would you like to proceed with the update?', 'update-confirmation', $instance->get_slug() )
1791 );
1792 }
1793
1794 if ( empty( $beta_data ) ) {
1795 return;
1796 }
1797 ?>
1798 <script type="text/javascript">
1799 ( function( $ ) {
1800 var betaData = <?php echo json_encode( $beta_data ) ?>;
1801
1802 for ( var pluginBasename in betaData ) {
1803 if ( ! betaData.hasOwnProperty( pluginBasename ) ) {
1804 continue;
1805 }
1806
1807 if ( ! betaData[ pluginBasename ].is_installed_version_beta ) {
1808 continue;
1809 }
1810
1811 var $parentContainer = $( '.wp-list-table.plugins tr[data-plugin="' + pluginBasename + '"]' );
1812 if ( 0 === $parentContainer.length ) {
1813 continue;
1814 }
1815
1816 $parentContainer.find( '.plugin-title > strong:first-child').append(
1817 '<span class="fs-tag fs-info"><?php fs_esc_js_echo_inline( 'Beta', 'beta' ) ?></span>'
1818 );
1819 }
1820
1821 setTimeout( function() {
1822 // Wait a little bit before adding the event handler, otherwise, it will be overridden by the core WP logic.
1823 $( '.plugins .update-message .update-link, .themes .theme .update-message' ).on( 'click', function() {
1824 var $parentContainer = $( this ).parents( 'tr:first' );
1825 pluginBasename = ( 0 !== $parentContainer.length ) ?
1826 $parentContainer.data( 'plugin' ) :
1827 $( this ).parents( '.theme:first' ).data( 'slug' );
1828
1829 if (
1830 betaData[ pluginBasename ] &&
1831 betaData[ pluginBasename ].beta_version_update_confirmation_message &&
1832 ! confirm( betaData[ pluginBasename ].beta_version_update_confirmation_message )
1833 ) {
1834 return false;
1835 }
1836 } );
1837 }, 20 );
1838 } )( jQuery );
1839 </script>
1840 <?php
1841 }
1842
1843 /**
1844 * Keeping the uninstall hook registered for free or premium plugin version may result to a fatal error that
1845 * could happen when a user tries to uninstall either version while one of them is still active. Uninstalling a
1846 * plugin will trigger inclusion of the free or premium version and if one of them is active during the
1847 * uninstallation, a fatal error may occur in case the plugin's class or functions are already defined.
1848 *
1849 * @author Leo Fajardo (@leorw)
1850 *
1851 * @since 1.2.0
1852 */
1853 private function unregister_uninstall_hook() {
1854 $uninstallable_plugins = (array) get_option( 'uninstall_plugins' );
1855 unset( $uninstallable_plugins[ $this->_free_plugin_basename ] );
1856 unset( $uninstallable_plugins[ $this->premium_plugin_basename() ] );
1857
1858 update_option( 'uninstall_plugins', $uninstallable_plugins );
1859 }
1860
1861 /**
1862 * @since 1.2.0 Invalidate module's main file cache, otherwise, FS_Plugin_Updater will not fetch updates.
1863 *
1864 * @param bool $store_prev_path
1865 */
1866 private function clear_module_main_file_cache( $store_prev_path = true ) {
1867 if ( ! isset( $this->_storage->plugin_main_file ) ||
1868 empty( $this->_storage->plugin_main_file->path )
1869 ) {
1870 return;
1871 }
1872
1873 if ( ! $store_prev_path ) {
1874 /**
1875 * Storing the previous path is not needed when clearing the cache after an SDK version update since
1876 * the main purpose of the cache clearing in that event is to correct a wrong plugin main file path
1877 * which causes data mix-up between plugins (e.g. titles and versions of an add-on and its parent plugin).
1878 *
1879 * @author Leo Fajardo (@leorw)
1880 * @since 2.2.1
1881 */
1882 unset( $this->_storage->plugin_main_file->path );
1883 } else {
1884 $plugin_main_file = clone $this->_storage->plugin_main_file;
1885
1886 // Store cached path (2nd layer cache).
1887 $plugin_main_file->prev_path = $plugin_main_file->path;
1888
1889 // Clear cached path.
1890 unset( $plugin_main_file->path );
1891
1892 $this->_storage->plugin_main_file = $plugin_main_file;
1893 }
1894
1895 /**
1896 * Clear global cached path.
1897 *
1898 * @author Leo Fajardo (@leorw)
1899 * @since 1.2.2
1900 */
1901 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map' );
1902 unset( $id_slug_type_path_map[ $this->_module_id ]['path'] );
1903 self::$_accounts->set_option( 'id_slug_type_path_map', $id_slug_type_path_map, true );
1904 }
1905
1906 /**
1907 * @author Leo Fajardo (@leorw)
1908 * @since 2.0.0
1909 */
1910 function _hook_action_links_and_register_account_hooks() {
1911 add_action( 'admin_init', array( &$this, '_add_tracking_links' ) );
1912
1913 if ( self::is_plugins_page() && $this->is_plugin() ) {
1914 $this->hook_plugin_action_links();
1915 }
1916
1917 $this->_register_account_hooks();
1918 }
1919
1920 /**
1921 * @author Vova Feldman (@svovaf)
1922 * @since 1.0.9
1923 */
1924 private function _register_account_hooks() {
1925 if ( ! is_admin() ) {
1926 return;
1927 }
1928
1929 /**
1930 * Always show the deactivation feedback form since we added
1931 * automatic free version deactivation upon premium code activation.
1932 *
1933 * @since 1.2.1.6
1934 */
1935 $this->add_ajax_action(
1936 'submit_uninstall_reason',
1937 array( &$this, '_submit_uninstall_reason_action' )
1938 );
1939
1940 $this->add_ajax_action(
1941 'cancel_subscription_or_trial',
1942 array( &$this, 'cancel_subscription_or_trial_ajax_action' )
1943 );
1944
1945 if ( ! $this->is_addon() || $this->is_parent_plugin_installed() ) {
1946 if ( ( $this->is_plugin() && self::is_plugins_page() ) ||
1947 ( $this->is_theme() && self::is_themes_page() )
1948 ) {
1949 add_action( 'admin_footer', array( &$this, '_add_deactivation_feedback_dialog_box' ) );
1950 }
1951 }
1952 }
1953
1954 /**
1955 * Leverage backtrace to find caller plugin file path.
1956 *
1957 * @author Vova Feldman (@svovaf)
1958 * @since 1.0.6
1959 *
1960 * @param bool $is_init Is initiation sequence.
1961 *
1962 * @return string
1963 */
1964 private function _find_caller_plugin_file( $is_init = false ) {
1965 // Try to load the cached value of the file path.
1966 if ( isset( $this->_storage->plugin_main_file ) ) {
1967 $plugin_main_file = $this->_storage->plugin_main_file;
1968 if ( ! empty( $plugin_main_file->path ) ) {
1969 $absolute_path = $this->get_absolute_path( $plugin_main_file->path );
1970 if ( file_exists( $absolute_path ) ) {
1971 return $absolute_path;
1972 }
1973 }
1974 }
1975
1976 /**
1977 * @since 1.2.1
1978 *
1979 * `clear_module_main_file_cache()` is clearing the plugin's cached path on
1980 * deactivation. Therefore, if any plugin/theme was initiating `Freemius`
1981 * with that plugin's slug, it was overriding the empty plugin path with a wrong path.
1982 *
1983 * So, we've added a special mechanism with a 2nd layer of cache that uses `prev_path`
1984 * when the class instantiator isn't the module.
1985 */
1986 if ( ! $is_init ) {
1987 // Fetch prev path cache.
1988 if ( isset( $this->_storage->plugin_main_file ) &&
1989 ! empty( $this->_storage->plugin_main_file->prev_path )
1990 ) {
1991 $absolute_path = $this->get_absolute_path( $this->_storage->plugin_main_file->prev_path );
1992 if ( file_exists( $absolute_path ) ) {
1993 return $absolute_path;
1994 }
1995 }
1996
1997 wp_die(
1998 $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' ) .
1999 " Module: {$this->_slug}; SDK: " . WP_FS__SDK_VERSION . ";",
2000 $this->get_text_inline( 'Error', 'error' ),
2001 array( 'back_link' => true )
2002 );
2003 }
2004
2005 /**
2006 * @since 1.2.1
2007 *
2008 * Only the original instantiator that calls dynamic_init can modify the module's path.
2009 */
2010 // Find caller module.
2011 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
2012 $this->_storage->plugin_main_file = (object) array(
2013 'path' => $id_slug_type_path_map[ $this->_module_id ]['path'],
2014 );
2015
2016 return $this->get_absolute_path( $id_slug_type_path_map[ $this->_module_id ]['path'] );
2017 }
2018
2019 /**
2020 * @author Leo Fajardo (@leorw)
2021 * @since 1.2.3
2022 *
2023 * @param string $path
2024 *
2025 * @return string
2026 */
2027 private function get_relative_path( $path ) {
2028 $module_root_dir = $this->get_module_root_dir_path();
2029 if ( 0 === strpos( $path, $module_root_dir ) ) {
2030 $path = substr( $path, strlen( $module_root_dir ) );
2031 }
2032
2033 return $path;
2034 }
2035
2036 /**
2037 * @author Leo Fajardo (@leorw)
2038 * @since 1.2.3
2039 *
2040 * @param string $path
2041 * @param string|bool $module_type
2042 *
2043 * @return string
2044 */
2045 private function get_absolute_path( $path, $module_type = false ) {
2046 $module_root_dir = $this->get_module_root_dir_path( $module_type );
2047 if ( 0 !== strpos( $path, $module_root_dir ) ) {
2048 $path = fs_normalize_path( $module_root_dir . $path );
2049 }
2050
2051 return $path;
2052 }
2053
2054 /**
2055 * @author Leo Fajardo (@leorw)
2056 * @since 1.2.3
2057 *
2058 * @param string|bool $module_type
2059 *
2060 * @return string
2061 */
2062 private function get_module_root_dir_path( $module_type = false ) {
2063 $is_plugin = empty( $module_type ) ?
2064 $this->is_plugin() :
2065 ( WP_FS__MODULE_TYPE_PLUGIN === $module_type );
2066
2067 return fs_normalize_path( trailingslashit( $is_plugin ?
2068 WP_PLUGIN_DIR :
2069 get_theme_root( get_stylesheet() ) ) );
2070 }
2071
2072 /**
2073 * @author Leo Fajardo (@leorw)
2074 *
2075 * @param number $module_id
2076 * @param string $slug
2077 *
2078 * @since 1.2.2
2079 */
2080 private function store_id_slug_type_path_map( $module_id, $slug ) {
2081 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
2082
2083 $store_option = false;
2084
2085 if ( ! isset( $id_slug_type_path_map[ $module_id ] ) ) {
2086 $id_slug_type_path_map[ $module_id ] = array(
2087 'slug' => $slug
2088 );
2089
2090 $store_option = true;
2091 }
2092
2093 if ( empty( $id_slug_type_path_map[ $module_id ]['path'] ) ||
2094 /**
2095 * This verification is for cases when suddenly the same module
2096 * is installed but with a different folder name.
2097 *
2098 * @author Vova Feldman (@svovaf)
2099 * @since 1.2.3
2100 */
2101 ! file_exists( $this->get_absolute_path(
2102 $id_slug_type_path_map[ $module_id ]['path'],
2103 $id_slug_type_path_map[ $module_id ]['type']
2104 ) )
2105 ) {
2106 $caller_main_file_and_type = $this->get_caller_main_file_and_type();
2107
2108 $id_slug_type_path_map[ $module_id ]['type'] = $caller_main_file_and_type->module_type;
2109 $id_slug_type_path_map[ $module_id ]['path'] = $caller_main_file_and_type->path;
2110
2111 $store_option = true;
2112 }
2113
2114 if ( $store_option ) {
2115 self::$_accounts->set_option( 'id_slug_type_path_map', $id_slug_type_path_map, true );
2116 }
2117 }
2118
2119 /**
2120 * Identifies the caller type: plugin or theme.
2121 *
2122 * @author Leo Fajardo (@leorw)
2123 * @since 1.2.2
2124 *
2125 * @author Vova Feldman (@svovaf)
2126 * @since 1.2.2.3 Find the earliest module in the call stack that calls to the SDK. This fix is for cases when
2127 * add-ons are relying on loading the SDK from the parent module, and also allows themes including the
2128 * SDK an internal file instead of directly from functions.php.
2129 * @since 1.2.1.7 Knows how to handle cases when an add-on includes the parent module logic.
2130 */
2131 private function get_caller_main_file_and_type() {
2132 self::require_plugin_essentials();
2133
2134 $all_plugins = fs_get_plugins( true );
2135 $all_plugins_paths = array();
2136
2137 // Get active plugin's main files real full names (might be symlinks).
2138 foreach ( $all_plugins as $relative_path => $data ) {
2139 if ( false === strpos( fs_normalize_path( $relative_path ), '/' ) ) {
2140 /**
2141 * Ignore plugins that don't have a folder (e.g. Hello Dolly) since they
2142 * can't really include the SDK.
2143 *
2144 * @author Vova Feldman
2145 * @since 1.2.1.7
2146 */
2147 continue;
2148 }
2149
2150 $all_plugins_paths[] = fs_normalize_path( realpath( WP_PLUGIN_DIR . '/' . $relative_path ) );
2151 }
2152
2153 $caller_file_candidate = false;
2154 $caller_map = array();
2155 $module_type = WP_FS__MODULE_TYPE_PLUGIN;
2156 $themes_dir = fs_normalize_path( get_theme_root( get_stylesheet() ) );
2157 $plugin_dir_to_skip = false;
2158
2159 for ( $i = 1, $bt = debug_backtrace(), $len = count( $bt ); $i < $len; $i ++ ) {
2160 if ( empty( $bt[ $i ]['file'] ) ) {
2161 continue;
2162 }
2163
2164 if ( $i > 1 && ! empty( $bt[ $i - 1 ]['file'] ) && $bt[ $i ]['file'] === $bt[ $i - 1 ]['file'] ) {
2165 // If file same as the prev file in the stack, skip it.
2166 continue;
2167 }
2168
2169 if ( ! empty( $bt[ $i ]['function'] ) && in_array( $bt[ $i ]['function'], array(
2170 'do_action',
2171 'apply_filter',
2172 // The string split is stupid, but otherwise, theme check
2173 // throws info notices.
2174 'requir' . 'e_once',
2175 'requir' . 'e',
2176 'includ' . 'e_once',
2177 'includ' . 'e',
2178 'install_and_activate_plugin',
2179 'try_activate_plugin',
2180 'activate_plugin'
2181 ) )
2182 ) {
2183 if ( 'activate_plugin' === $bt[ $i ]['function'] ) {
2184 /**
2185 * Store the directory of the activator plugin so that any other file that starts with it
2186 * cannot be mistakenly chosen as a candidate caller file.
2187 *
2188 * @author Leo Fajardo
2189 *
2190 * @since 2.3.0
2191 */
2192 $caller_file_path = fs_normalize_path( $bt[ $i ]['file'] );
2193
2194 foreach ( $all_plugins_paths as $plugin_path ) {
2195 $plugin_dir = fs_normalize_path( dirname( $plugin_path ) . '/' );
2196 if ( false !== strpos( $caller_file_path, $plugin_dir ) ) {
2197 $plugin_dir_to_skip = $plugin_dir;
2198
2199 break;
2200 }
2201 }
2202 }
2203
2204 // Ignore call stack hooks and files inclusion.
2205 continue;
2206 }
2207
2208 $caller_file_path = fs_normalize_path( $bt[ $i ]['file'] );
2209
2210 if ( ! empty( $plugin_dir_to_skip ) ) {
2211 /**
2212 * Skip if it's an activator plugin file to avoid mistakenly choosing it as a candidate caller file.
2213 *
2214 * @author Leo Fajardo
2215 *
2216 * @since 2.3.0
2217 */
2218 if ( 0 === strpos( $caller_file_path, $plugin_dir_to_skip ) ) {
2219 continue;
2220 }
2221 }
2222
2223 if ( 'functions.php' === basename( $caller_file_path ) ) {
2224 /**
2225 * 1. Assumes that theme's starting execution file is functions.php.
2226 * 2. This complex logic fixes symlink issues (e.g. with Vargant).
2227 *
2228 * @author Vova Feldman (@svovaf)
2229 * @since 1.2.2.5
2230 */
2231
2232 if ( $caller_file_path == fs_normalize_path( realpath( trailingslashit( $themes_dir ) . basename( dirname( $caller_file_path ) ) . '/' . basename( $caller_file_path ) ) ) ) {
2233 $module_type = WP_FS__MODULE_TYPE_THEME;
2234
2235 /**
2236 * Relative path of the theme, e.g.:
2237 * `my-theme/functions.php`
2238 *
2239 * @author Leo Fajardo (@leorw)
2240 */
2241 $caller_file_candidate = basename( dirname( $caller_file_path ) ) .
2242 '/' .
2243 basename( $caller_file_path );
2244
2245 continue;
2246 }
2247 }
2248
2249 $caller_file_hash = md5( $caller_file_path );
2250
2251 if ( ! isset( $caller_map[ $caller_file_hash ] ) ) {
2252 foreach ( $all_plugins_paths as $plugin_path ) {
2253 if ( false !== strpos( $caller_file_path, fs_normalize_path( dirname( $plugin_path ) . '/' ) ) ) {
2254 $caller_map[ $caller_file_hash ] = fs_normalize_path( $plugin_path );
2255 break;
2256 }
2257 }
2258 }
2259
2260 if ( isset( $caller_map[ $caller_file_hash ] ) ) {
2261 $module_type = WP_FS__MODULE_TYPE_PLUGIN;
2262 $caller_file_candidate = plugin_basename( $caller_map[ $caller_file_hash ] );
2263 }
2264 }
2265
2266 return (object) array(
2267 'module_type' => $module_type,
2268 'path' => $caller_file_candidate
2269 );
2270 }
2271
2272 #----------------------------------------------------------------------------------
2273 #region Deactivation Feedback Form
2274 #----------------------------------------------------------------------------------
2275
2276 /**
2277 * Displays a confirmation and feedback dialog box when the user clicks on the "Deactivate" link on the plugins
2278 * page.
2279 *
2280 * @author Vova Feldman (@svovaf)
2281 * @author Leo Fajardo (@leorw)
2282 *
2283 * @since 1.1.2
2284 */
2285 function _add_deactivation_feedback_dialog_box() {
2286 $subscription_cancellation_dialog_box_template_params = $this->apply_filters( 'show_deactivation_subscription_cancellation', true ) ?
2287 $this->_get_subscription_cancellation_dialog_box_template_params() :
2288 array();
2289
2290 /**
2291 * @since 2.3.0 Developers can optionally hide the deactivation feedback form using the 'show_deactivation_feedback_form' filter.
2292 */
2293 $show_deactivation_feedback_form = true;
2294 if ( $this->has_filter( 'show_deactivation_feedback_form' ) ) {
2295 $show_deactivation_feedback_form = $this->apply_filters( 'show_deactivation_feedback_form', true );
2296 } else if ( $this->is_addon() ) {
2297 /**
2298 * If the add-on's 'show_deactivation_feedback_form' is not set, try to inherit the value from the parent.
2299 */
2300 $show_deactivation_feedback_form = $this->get_parent_instance()->apply_filters( 'show_deactivation_feedback_form', true );
2301 }
2302
2303 $uninstall_confirmation_message = $this->apply_filters( 'uninstall_confirmation_message', '' );
2304
2305 if (
2306 empty( $subscription_cancellation_dialog_box_template_params ) &&
2307 ! $show_deactivation_feedback_form &&
2308 empty( $uninstall_confirmation_message )
2309 ) {
2310 return;
2311 }
2312
2313 $vars = array( 'id' => $this->_module_id );
2314
2315 if ( $show_deactivation_feedback_form ) {
2316 /* Check the type of user:
2317 * 1. Long-term (long-term)
2318 * 2. Non-registered and non-anonymous short-term (non-registered-and-non-anonymous-short-term).
2319 * 3. Short-term (short-term)
2320 */
2321 $is_long_term_user = true;
2322
2323 // Check if the site is at least 2 days old.
2324 $time_installed = $this->_storage->install_timestamp;
2325
2326 // Difference in seconds.
2327 $date_diff = time() - $time_installed;
2328
2329 // Convert seconds to days.
2330 $date_diff_days = floor( $date_diff / ( 60 * 60 * 24 ) );
2331
2332 if ( $date_diff_days < 2 ) {
2333 $is_long_term_user = false;
2334 }
2335
2336 $is_long_term_user = $this->apply_filters( 'is_long_term_user', $is_long_term_user );
2337
2338 if ( $is_long_term_user ) {
2339 $user_type = 'long-term';
2340 } else {
2341 if ( ! $this->is_registered() && ! $this->is_anonymous() ) {
2342 $user_type = 'non-registered-and-non-anonymous-short-term';
2343 } else {
2344 $user_type = 'short-term';
2345 }
2346 }
2347
2348 $uninstall_reasons = $this->_get_uninstall_reasons( $user_type );
2349
2350 $vars['reasons'] = $uninstall_reasons;
2351 }
2352
2353 $vars['subscription_cancellation_dialog_box_template_params'] = &$subscription_cancellation_dialog_box_template_params;
2354 $vars['show_deactivation_feedback_form'] = $show_deactivation_feedback_form;
2355 $vars['uninstall_confirmation_message'] = $uninstall_confirmation_message;
2356
2357 /**
2358 * Load the HTML template for the deactivation feedback dialog box.
2359 *
2360 * @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.
2361 */
2362 fs_require_template( 'forms/deactivation/form.php', $vars );
2363 }
2364
2365 /**
2366 * @author Leo Fajardo (@leorw)
2367 * @since 1.1.2
2368 *
2369 * @param string $user_type
2370 *
2371 * @return array The uninstall reasons for the specified user type.
2372 */
2373 function _get_uninstall_reasons( $user_type = 'long-term' ) {
2374 $module_type = $this->_module_type;
2375
2376 $internal_message_template_var = array(
2377 'id' => $this->_module_id
2378 );
2379
2380 $plan = $this->get_plan();
2381
2382 if ( $this->is_registered() && is_object( $plan ) && $plan->has_technical_support() ) {
2383 $contact_support_template = fs_get_template( 'forms/deactivation/contact.php', $internal_message_template_var );
2384 } else {
2385 $contact_support_template = '';
2386 }
2387
2388 $reason_found_better_plugin = array(
2389 'id' => self::REASON_FOUND_A_BETTER_PLUGIN,
2390 'text' => sprintf( $this->get_text_inline( 'I found a better %s', 'reason-found-a-better-plugin' ), $module_type ),
2391 'input_type' => 'textfield',
2392 'input_placeholder' => sprintf( $this->get_text_inline( "What's the %s's name?", 'placeholder-plugin-name' ), $module_type ),
2393 );
2394
2395 $reason_temporary_deactivation = array(
2396 'id' => self::REASON_TEMPORARY_DEACTIVATION,
2397 'text' => sprintf(
2398 $this->get_text_inline( "It's a temporary %s. I'm just debugging an issue.", 'reason-temporary-x' ),
2399 strtolower( $this->is_plugin() ?
2400 $this->get_text_inline( 'Deactivation', 'deactivation' ) :
2401 $this->get_text_inline( 'Theme Switch', 'theme-switch' )
2402 )
2403 ),
2404 'input_type' => '',
2405 'input_placeholder' => ''
2406 );
2407
2408 $reason_other = array(
2409 'id' => self::REASON_OTHER,
2410 'text' => $this->get_text_inline( 'Other', 'reason-other' ),
2411 'input_type' => 'textfield',
2412 'input_placeholder' => ''
2413 );
2414
2415 $long_term_user_reasons = array(
2416 array(
2417 'id' => self::REASON_NO_LONGER_NEEDED,
2418 'text' => sprintf( $this->get_text_inline( 'I no longer need the %s', 'reason-no-longer-needed' ), $module_type ),
2419 'input_type' => '',
2420 'input_placeholder' => ''
2421 ),
2422 $reason_found_better_plugin,
2423 array(
2424 'id' => self::REASON_NEEDED_FOR_A_SHORT_PERIOD,
2425 'text' => sprintf( $this->get_text_inline( 'I only needed the %s for a short period', 'reason-needed-for-a-short-period' ), $module_type ),
2426 'input_type' => '',
2427 'input_placeholder' => ''
2428 ),
2429 array(
2430 'id' => self::REASON_BROKE_MY_SITE,
2431 'text' => sprintf( $this->get_text_inline( 'The %s broke my site', 'reason-broke-my-site' ), $module_type ),
2432 'input_type' => '',
2433 'input_placeholder' => '',
2434 'internal_message' => $contact_support_template
2435 ),
2436 array(
2437 'id' => self::REASON_SUDDENLY_STOPPED_WORKING,
2438 'text' => sprintf( $this->get_text_inline( 'The %s suddenly stopped working', 'reason-suddenly-stopped-working' ), $module_type ),
2439 'input_type' => '',
2440 'input_placeholder' => '',
2441 'internal_message' => $contact_support_template
2442 )
2443 );
2444
2445 if ( $this->is_paying() ) {
2446 $long_term_user_reasons[] = array(
2447 'id' => self::REASON_CANT_PAY_ANYMORE,
2448 'text' => $this->get_text_inline( "I can't pay for it anymore", 'reason-cant-pay-anymore' ),
2449 'input_type' => 'textfield',
2450 'input_placeholder' => $this->get_text_inline( 'What price would you feel comfortable paying?', 'placeholder-comfortable-price' )
2451 );
2452 }
2453
2454 $reason_dont_share_info = array(
2455 'id' => self::REASON_DONT_LIKE_TO_SHARE_MY_INFORMATION,
2456 'text' => $this->get_text_inline( "I don't like to share my information with you", 'reason-dont-like-to-share-my-information' ),
2457 'input_type' => '',
2458 'input_placeholder' => ''
2459 );
2460
2461 /**
2462 * If the current user has selected the "don't share data" reason in the deactivation feedback modal, inform the
2463 * user by showing additional message that he doesn't have to share data and can just choose to skip the opt-in
2464 * (the Skip button is included in the message to show). This message will only be shown if anonymous mode is
2465 * enabled and the user's account is currently not in pending activation state (similar to the way the Skip
2466 * button in the opt-in form is shown/hidden).
2467 */
2468 if ( $this->is_enable_anonymous() && ! $this->is_pending_activation() ) {
2469 $reason_dont_share_info['internal_message'] = fs_get_template( 'forms/deactivation/retry-skip.php', $internal_message_template_var );
2470 }
2471
2472 $uninstall_reasons = array(
2473 'long-term' => $long_term_user_reasons,
2474 'non-registered-and-non-anonymous-short-term' => array(
2475 array(
2476 'id' => self::REASON_DIDNT_WORK,
2477 'text' => sprintf( $this->get_text_inline( "The %s didn't work", 'reason-didnt-work' ), $module_type ),
2478 'input_type' => '',
2479 'input_placeholder' => ''
2480 ),
2481 $reason_dont_share_info,
2482 $reason_found_better_plugin
2483 ),
2484 'short-term' => array(
2485 array(
2486 'id' => self::REASON_COULDNT_MAKE_IT_WORK,
2487 'text' => $this->get_text_inline( "I couldn't understand how to make it work", 'reason-couldnt-make-it-work' ),
2488 'input_type' => '',
2489 'input_placeholder' => '',
2490 'internal_message' => $contact_support_template
2491 ),
2492 $reason_found_better_plugin,
2493 array(
2494 'id' => self::REASON_GREAT_BUT_NEED_SPECIFIC_FEATURE,
2495 '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 ),
2496 'input_type' => 'textarea',
2497 'input_placeholder' => $this->get_text_inline( 'What feature?', 'placeholder-feature' )
2498 ),
2499 array(
2500 'id' => self::REASON_NOT_WORKING,
2501 'text' => sprintf( $this->get_text_inline( 'The %s is not working', 'reason-not-working' ), $module_type ),
2502 'input_type' => 'textarea',
2503 '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' )
2504 ),
2505 array(
2506 'id' => self::REASON_NOT_WHAT_I_WAS_LOOKING_FOR,
2507 'text' => $this->get_text_inline( "It's not what I was looking for", 'reason-not-what-i-was-looking-for' ),
2508 'input_type' => 'textarea',
2509 'input_placeholder' => $this->get_text_inline( "What you've been looking for?", 'placeholder-what-youve-been-looking-for' )
2510 ),
2511 array(
2512 'id' => self::REASON_DIDNT_WORK_AS_EXPECTED,
2513 'text' => sprintf( $this->get_text_inline( "The %s didn't work as expected", 'reason-didnt-work-as-expected' ), $module_type ),
2514 'input_type' => 'textarea',
2515 'input_placeholder' => $this->get_text_inline( 'What did you expect?', 'placeholder-what-did-you-expect' )
2516 )
2517 )
2518 );
2519
2520 // Randomize the reasons for the current user type.
2521 shuffle( $uninstall_reasons[ $user_type ] );
2522
2523 // Keep the following reasons as the last items in the list.
2524 $uninstall_reasons[ $user_type ][] = $reason_temporary_deactivation;
2525 $uninstall_reasons[ $user_type ][] = $reason_other;
2526
2527 $uninstall_reasons = $this->apply_filters( 'uninstall_reasons', $uninstall_reasons );
2528
2529 return $uninstall_reasons[ $user_type ];
2530 }
2531
2532 /**
2533 * Called after the user has submitted his reason for deactivating the plugin.
2534 *
2535 * @author Leo Fajardo (@leorw)
2536 * @since 1.1.2
2537 */
2538 function _submit_uninstall_reason_action() {
2539 $this->_logger->entrance();
2540
2541 $this->check_ajax_referer( 'submit_uninstall_reason' );
2542
2543 $reason_id = fs_request_get( 'reason_id' );
2544
2545 // Check if the given reason ID is an unsigned integer.
2546 if ( ! ctype_digit( $reason_id ) ) {
2547 exit;
2548 }
2549
2550 $reason_info = trim( fs_request_get( 'reason_info', '' ) );
2551 if ( ! empty( $reason_info ) ) {
2552 $reason_info = substr( $reason_info, 0, 128 );
2553 }
2554
2555 $reason = (object) array(
2556 'id' => $reason_id,
2557 'info' => $reason_info,
2558 'is_anonymous' => fs_request_get_bool( 'is_anonymous' )
2559 );
2560
2561 $this->_storage->store( 'uninstall_reason', $reason );
2562
2563 /**
2564 * If the module type is "theme", trigger the uninstall event here (on theme deactivation) since themes do
2565 * not support uninstall hook.
2566 *
2567 * @author Leo Fajardo (@leorw)
2568 * @since 1.2.2
2569 */
2570 if ( $this->is_theme() ) {
2571 if ( $this->is_premium() && ! $this->has_active_valid_license() ) {
2572 FS_Plugin_Updater::instance( $this )->delete_update_data();
2573 }
2574
2575 $this->_uninstall_plugin_event( false );
2576 $this->remove_sdk_reference();
2577 }
2578
2579 // Print '1' for successful operation.
2580 echo 1;
2581 exit;
2582 }
2583
2584 /**
2585 * @author Leo Fajardo (@leorw)
2586 * @since 2.1.4
2587 */
2588 function cancel_subscription_or_trial_ajax_action() {
2589 $this->_logger->entrance();
2590
2591 $this->check_ajax_referer( 'cancel_subscription_or_trial' );
2592
2593 $result = $this->cancel_subscription_or_trial( fs_request_get( 'plugin_id', $this->get_id() ), false );
2594
2595 if ( $this->is_api_error( $result ) ) {
2596 $this->shoot_ajax_failure( $result->error->message );
2597 }
2598
2599 $this->shoot_ajax_success();
2600 }
2601
2602 /**
2603 * @author Leo Fajardo (@leorw)
2604 * @since 2.1.4
2605 *
2606 * @param number $plugin_id
2607 *
2608 * @return object
2609 */
2610 private function cancel_subscription_or_trial( $plugin_id ) {
2611 $fs = null;
2612 if ( $plugin_id == $this->get_id() ) {
2613 $fs = $this;
2614 } else if ( $this->is_addon_activated( $plugin_id ) ) {
2615 $fs = self::get_instance_by_id( $plugin_id );
2616 }
2617
2618 $result = null;
2619
2620 if ( ! is_null( $fs ) ) {
2621 $result = $fs->is_paid_trial() ?
2622 $fs->_cancel_trial() :
2623 $fs->_downgrade_site();
2624 }
2625
2626 return $result;
2627 }
2628
2629 /**
2630 * @author Leo Fajardo (@leorw)
2631 * @since 2.0.2
2632 */
2633 function _delete_theme_update_data_action() {
2634 FS_Plugin_Updater::instance( $this )->delete_update_data();
2635 }
2636
2637 #endregion
2638
2639 #----------------------------------------------------------------------------------
2640 #region Instance
2641 #----------------------------------------------------------------------------------
2642
2643 /**
2644 * Main singleton instance.
2645 *
2646 * @author Vova Feldman (@svovaf)
2647 * @since 1.0.0
2648 *
2649 * @param number $module_id
2650 * @param string|bool $slug
2651 * @param bool $is_init Is initiation sequence.
2652 *
2653 * @return Freemius|false
2654 */
2655 static function instance( $module_id, $slug = false, $is_init = false ) {
2656 if ( empty( $module_id ) ) {
2657 return false;
2658 }
2659
2660 /**
2661 * 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.
2662 */
2663 self::_load_required_static();
2664
2665 if ( ! is_numeric( $module_id ) ) {
2666 if ( ! $is_init && true === $slug ) {
2667 $is_init = true;
2668 }
2669
2670 $slug = $module_id;
2671
2672 $module = FS_Plugin_Manager::instance( $slug )->get();
2673
2674 if ( is_object( $module ) ) {
2675 $module_id = $module->id;
2676 }
2677 }
2678
2679 $key = 'm_' . $module_id;
2680
2681 if ( ! isset( self::$_instances[ $key ] ) ) {
2682 self::$_instances[ $key ] = new Freemius( $module_id, $slug, $is_init );
2683 }
2684
2685 return self::$_instances[ $key ];
2686 }
2687
2688 /**
2689 * @author Vova Feldman (@svovaf)
2690 * @since 1.0.6
2691 *
2692 * @param number $addon_id
2693 *
2694 * @return bool
2695 */
2696 private static function has_instance( $addon_id ) {
2697 return isset( self::$_instances[ 'm_' . $addon_id ] );
2698 }
2699
2700 /**
2701 * @author Leo Fajardo (@leorw)
2702 * @since 1.2.2
2703 *
2704 * @param string|number $id_or_slug
2705 * @param string $module_type
2706 *
2707 * @return number|false
2708 */
2709 private static function get_module_id( $id_or_slug, $module_type = WP_FS__MODULE_TYPE_PLUGIN ) {
2710 if ( is_numeric( $id_or_slug ) ) {
2711 return $id_or_slug;
2712 }
2713
2714 foreach ( self::$_instances as $instance ) {
2715 // Also check the module type since there can be a plugin and a theme with the same slug.
2716 if ( ( $module_type === $instance->get_module_type() ) && ( $id_or_slug === $instance->get_slug() ) ) {
2717 return $instance->get_id();
2718 }
2719 }
2720
2721 return false;
2722 }
2723
2724 /**
2725 * @author Vova Feldman (@svovaf)
2726 * @since 1.0.6
2727 *
2728 * @param number $id
2729 *
2730 * @return false|Freemius
2731 */
2732 static function get_instance_by_id( $id ) {
2733 return isset ( self::$_instances[ 'm_' . $id ] ) ?
2734 self::$_instances[ 'm_' . $id ] :
2735 false;
2736 }
2737
2738 /**
2739 *
2740 * @author Vova Feldman (@svovaf)
2741 * @since 1.0.1
2742 *
2743 * @param string $plugin_file
2744 * @param string $module_type
2745 *
2746 * @return false|Freemius
2747 */
2748 static function get_instance_by_file( $plugin_file, $module_type = WP_FS__MODULE_TYPE_PLUGIN ) {
2749 $slug = self::find_slug_by_basename( $plugin_file );
2750
2751 return ( false !== $slug ) ?
2752 self::instance( self::get_module_id( $slug, $module_type ) ) :
2753 false;
2754 }
2755
2756 /**
2757 * @author Vova Feldman (@svovaf)
2758 * @since 1.0.6
2759 *
2760 * @return false|Freemius
2761 */
2762 function get_parent_instance() {
2763 return self::get_instance_by_id( $this->_plugin->parent_plugin_id );
2764 }
2765
2766 /**
2767 * @author Vova Feldman (@svovaf)
2768 * @since 1.0.6
2769 *
2770 * @param string|number $id_or_slug
2771 *
2772 * @return false|Freemius
2773 */
2774 function get_addon_instance( $id_or_slug ) {
2775 $addon_id = self::get_module_id( $id_or_slug );
2776
2777 return self::instance( $addon_id );
2778 }
2779
2780 #endregion ------------------------------------------------------------------
2781
2782 /**
2783 * @author Vova Feldman (@svovaf)
2784 * @since 1.0.6
2785 *
2786 * @return bool
2787 */
2788 function is_parent_plugin_installed() {
2789 $is_active = self::has_instance( $this->_plugin->parent_plugin_id );
2790
2791 if ( $is_active ) {
2792 return true;
2793 }
2794
2795 /**
2796 * Parent module might be a theme. If that's the case, the add-on's FS
2797 * instance will be loaded prior to the theme's FS instance, therefore,
2798 * we need to check if it's active with a "look ahead".
2799 *
2800 * @author Vova Feldman
2801 * @since 1.2.2.3
2802 */
2803 global $fs_active_plugins;
2804 if ( is_object( $fs_active_plugins ) && is_array( $fs_active_plugins->plugins ) ) {
2805 $active_theme = wp_get_theme();
2806
2807 foreach ( $fs_active_plugins->plugins as $sdk => $module ) {
2808 if ( WP_FS__MODULE_TYPE_THEME === $module->type ) {
2809 if ( $module->plugin_path == $active_theme->get_stylesheet() ) {
2810 // Parent module is a theme and it's currently active.
2811 return true;
2812 }
2813 }
2814 }
2815 }
2816
2817 return false;
2818 }
2819
2820 /**
2821 * Check if add-on parent plugin in activation mode.
2822 *
2823 * @author Vova Feldman (@svovaf)
2824 * @since 1.0.7
2825 *
2826 * @return bool
2827 */
2828 function is_parent_in_activation() {
2829 $parent_fs = $this->get_parent_instance();
2830 if ( ! is_object( $parent_fs ) ) {
2831 return false;
2832 }
2833
2834 return ( $parent_fs->is_activation_mode() );
2835 }
2836
2837 /**
2838 * Is plugin in activation mode.
2839 *
2840 * @author Vova Feldman (@svovaf)
2841 * @since 1.0.7
2842 *
2843 * @param bool $and_on
2844 *
2845 * @return bool
2846 */
2847 function is_activation_mode( $and_on = true ) {
2848 return fs_is_network_admin() ?
2849 $this->is_network_activation_mode( $and_on ) :
2850 $this->is_site_activation_mode( $and_on );
2851 }
2852
2853 /**
2854 * Is plugin in activation mode.
2855 *
2856 * @author Vova Feldman (@svovaf)
2857 * @since 1.0.7
2858 *
2859 * @param bool $and_on
2860 *
2861 * @return bool
2862 */
2863 function is_site_activation_mode( $and_on = true ) {
2864 return (
2865 ( $this->is_on() || ! $and_on ) &&
2866 (
2867 ( $this->is_premium() && true === $this->_storage->require_license_activation ) ||
2868 (
2869 ( ! $this->is_registered() ||
2870 ( $this->is_only_premium() && ! $this->has_features_enabled_license() ) ) &&
2871 ( ! $this->is_enable_anonymous() ||
2872 ( ! $this->is_anonymous() && ! $this->is_pending_activation() ) )
2873 )
2874 )
2875 );
2876 }
2877
2878 /**
2879 * Checks if the SDK in network activation mode.
2880 *
2881 * @author Leo Fajardo (@leorw)
2882 * @since 2.0.0
2883 *
2884 * @param bool $and_on
2885 *
2886 * @return bool
2887 */
2888 private function is_network_activation_mode( $and_on = true ) {
2889 if ( ! $this->_is_network_active ) {
2890 // Not network activated.
2891 return false;
2892 }
2893
2894 if ( $this->is_network_upgrade_mode() ) {
2895 // Special flag to enforce network activation mode to decide what to do with the sites that are not yet opted-in nor skipped.
2896 return true;
2897 }
2898
2899 if ( ! $this->is_site_activation_mode( $and_on ) ) {
2900 // 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.
2901 return false;
2902 }
2903
2904 if ( $this->is_network_delegated_connection() ) {
2905 // Super-admin delegated the connection to the site admins -> not activation mode.
2906 return false;
2907 }
2908
2909 if ( $this->is_network_anonymous() && true !== $this->_storage->require_license_activation ) {
2910 // Super-admin skipped the connection network wide -> not activation mode.
2911 return false;
2912 }
2913
2914 if ( $this->is_network_registered() ) {
2915 // Super-admin connected at least one site -> not activation mode.
2916 return false;
2917 }
2918
2919 return true;
2920 }
2921
2922 /**
2923 * Check if current page is the opt-in/pending-activation page.
2924 *
2925 * @author Vova Feldman (@svovaf)
2926 * @since 1.2.1.7
2927 *
2928 * @return bool
2929 */
2930 function is_activation_page() {
2931 if ( $this->_menu->is_main_settings_page() ) {
2932 return true;
2933 }
2934
2935 if ( ! $this->is_activation_mode() ) {
2936 return false;
2937 }
2938
2939 // Check if current page is matching the activation page.
2940 return $this->is_matching_url( $this->get_activation_url() );
2941 }
2942
2943 /**
2944 * Check if URL path's are matching and that all querystring
2945 * arguments of the $sub_url exist in the $url with the same values.
2946 *
2947 * WARNING:
2948 * 1. This method doesn't check if the sub/domain are matching.
2949 * 2. Ignore case sensitivity.
2950 *
2951 * @author Vova Feldman (@svovaf)
2952 * @since 1.2.1.7
2953 *
2954 * @param string $sub_url
2955 * @param string $url If argument is not set, check if the sub_url matching the current's page URL.
2956 *
2957 * @return bool
2958 */
2959 private function is_matching_url( $sub_url, $url = '' ) {
2960 if ( empty( $url ) ) {
2961 $url = $_SERVER['REQUEST_URI'];
2962 }
2963
2964 $url = strtolower( $url );
2965 $sub_url = strtolower( $sub_url );
2966
2967 if ( parse_url( $sub_url, PHP_URL_PATH ) !== parse_url( $url, PHP_URL_PATH ) ) {
2968 // Different path - DO NOT OVERRIDE PAGE.
2969 return false;
2970 }
2971
2972 $url_params = array();
2973 parse_str( parse_url( $url, PHP_URL_QUERY ), $url_params );
2974
2975 $sub_url_params = array();
2976 parse_str( parse_url( $sub_url, PHP_URL_QUERY ), $sub_url_params );
2977
2978 foreach ( $sub_url_params as $key => $val ) {
2979 if ( ! isset( $url_params[ $key ] ) || $val != $url_params[ $key ] ) {
2980 // Not matching query string - DO NOT OVERRIDE PAGE.
2981 return false;
2982 }
2983 }
2984
2985 return true;
2986 }
2987
2988 /**
2989 * Get the basenames of all active plugins for specific blog. Including network activated plugins.
2990 *
2991 * @author Vova Feldman (@svovaf)
2992 * @since 2.0.0
2993 *
2994 * @param int $blog_id
2995 *
2996 * @return string[]
2997 */
2998 private static function get_active_plugins_basenames( $blog_id = 0 ) {
2999 if ( is_multisite() && $blog_id > 0 ) {
3000 $active_basenames = get_blog_option( $blog_id, 'active_plugins' );
3001 } else {
3002 $active_basenames = get_option( 'active_plugins' );
3003 }
3004
3005 if ( ! is_array( $active_basenames ) ) {
3006 $active_basenames = array();
3007 }
3008
3009 if ( is_multisite() ) {
3010 $network_active_basenames = get_site_option( 'active_sitewide_plugins' );
3011
3012 if ( is_array( $network_active_basenames ) && ! empty( $network_active_basenames ) ) {
3013 $active_basenames = array_merge( $active_basenames, array_keys( $network_active_basenames ) );
3014 }
3015 }
3016
3017 return $active_basenames;
3018 }
3019
3020 /**
3021 * @author Leo Fajardo (@leorw)
3022 * @since 2.3.0
3023 *
3024 * @param int $blog_id
3025 *
3026 * @return array
3027 */
3028 static function get_active_plugins_directories_map( $blog_id = 0 ) {
3029 $active_basenames = self::get_active_plugins_basenames( $blog_id );
3030
3031 $map = array();
3032
3033 foreach ( $active_basenames as $active_basename ) {
3034 $active_basename = fs_normalize_path( $active_basename );
3035
3036 if ( false === strpos( $active_basename, '/' ) ) {
3037 continue;
3038 }
3039
3040 $map[ dirname( $active_basename ) ] = true;
3041 }
3042
3043 return $map;
3044 }
3045
3046 /**
3047 * Get collection of all active plugins. Including network activated plugins.
3048 *
3049 * @author Vova Feldman (@svovaf)
3050 * @since 1.0.9
3051 *
3052 * @param int $blog_id Since 2.0.0
3053 *
3054 * @return array[string]array
3055 */
3056 private static function get_active_plugins( $blog_id = 0 ) {
3057 self::require_plugin_essentials();
3058
3059 $active_plugin = array();
3060 $all_plugins = fs_get_plugins();
3061 $active_plugins_basenames = self::get_active_plugins_basenames( $blog_id );
3062
3063 foreach ( $active_plugins_basenames as $plugin_basename ) {
3064 $active_plugin[ $plugin_basename ] = $all_plugins[ $plugin_basename ];
3065 }
3066
3067 return $active_plugin;
3068 }
3069
3070 /**
3071 * Get collection of all site active plugins for a specified blog.
3072 *
3073 * @author Vova Feldman (@svovaf)
3074 * @since 2.0.0
3075 *
3076 * @param int $blog_id
3077 *
3078 * @return array[string]array
3079 */
3080 private static function get_site_active_plugins( $blog_id = 0 ) {
3081 $active_basenames = ( is_multisite() && $blog_id > 0 ) ?
3082 get_blog_option( $blog_id, 'active_plugins' ) :
3083 get_option( 'active_plugins' );
3084
3085 $active = array();
3086
3087 if ( ! is_array( $active_basenames ) ) {
3088 return $active;
3089 }
3090
3091 foreach ( $active_basenames as $basename ) {
3092 $active[ $basename ] = array(
3093 'is_active' => true,
3094 'Version' => '1.0', // Dummy version.
3095 'slug' => self::get_plugin_slug( $basename ),
3096 );
3097 }
3098
3099 return $active;
3100 }
3101
3102 /**
3103 * Get collection of all plugins with their activation status for a specified blog.
3104 *
3105 * @author Vova Feldman (@svovaf)
3106 * @since 1.1.8
3107 *
3108 * @param int $blog_id Since 2.0.0
3109 *
3110 * @return array Key is the plugin file path and the value is an array of the plugin data.
3111 */
3112 private static function get_all_plugins( $blog_id = 0 ) {
3113 self::require_plugin_essentials();
3114
3115 $all_plugins = fs_get_plugins();
3116
3117 $active_plugins_basenames = self::get_active_plugins_basenames( $blog_id );
3118
3119 foreach ( $all_plugins as $basename => &$data ) {
3120 // By default set to inactive (next foreach update the active plugins).
3121 $data['is_active'] = false;
3122 // Enrich with plugin slug.
3123 $data['slug'] = self::get_plugin_slug( $basename );
3124 }
3125
3126 // Flag active plugins.
3127 foreach ( $active_plugins_basenames as $basename ) {
3128 if ( isset( $all_plugins[ $basename ] ) ) {
3129 $all_plugins[ $basename ]['is_active'] = true;
3130 }
3131 }
3132
3133 return $all_plugins;
3134 }
3135
3136 /**
3137 * Get collection of all plugins and if they are network level activated.
3138 *
3139 * @author Vova Feldman (@svovaf)
3140 * @since 2.0.0
3141 *
3142 * @return array Key is the plugin basename and the value is an array of the plugin data.
3143 */
3144 private static function get_network_plugins() {
3145 self::require_plugin_essentials();
3146
3147 $all_plugins = fs_get_plugins();
3148
3149 $network_active_basenames = is_multisite() ?
3150 get_site_option( 'active_sitewide_plugins' ) :
3151 array();
3152
3153 foreach ( $all_plugins as $basename => &$data ) {
3154 // By default set to inactive (next foreach update the active plugins).
3155 $data['is_active'] = false;
3156 // Enrich with plugin slug.
3157 $data['slug'] = self::get_plugin_slug( $basename );
3158 }
3159
3160 // Flag active plugins.
3161 foreach ( $network_active_basenames as $basename ) {
3162 if ( isset( $all_plugins[ $basename ] ) ) {
3163 $all_plugins[ $basename ]['is_active'] = true;
3164 }
3165 }
3166
3167 return $all_plugins;
3168 }
3169
3170 /**
3171 * Cached result of get_site_transient( 'update_plugins' )
3172 *
3173 * @author Vova Feldman (@svovaf)
3174 * @since 1.1.8
3175 *
3176 * @var object
3177 */
3178 private static $_plugins_info;
3179
3180 /**
3181 * Helper function to get specified plugin's slug.
3182 *
3183 * @author Vova Feldman (@svovaf)
3184 * @since 1.1.8
3185 *
3186 * @param $basename
3187 *
3188 * @return string
3189 */
3190 private static function get_plugin_slug( $basename ) {
3191 if ( ! isset( self::$_plugins_info ) ) {
3192 self::$_plugins_info = get_site_transient( 'update_plugins' );
3193 }
3194
3195 $slug = '';
3196
3197 if ( is_object( self::$_plugins_info ) ) {
3198 if ( isset( self::$_plugins_info->no_update ) &&
3199 isset( self::$_plugins_info->no_update[ $basename ] ) &&
3200 ! empty( self::$_plugins_info->no_update[ $basename ]->slug )
3201 ) {
3202 $slug = self::$_plugins_info->no_update[ $basename ]->slug;
3203 } else if ( isset( self::$_plugins_info->response ) &&
3204 isset( self::$_plugins_info->response[ $basename ] ) &&
3205 ! empty( self::$_plugins_info->response[ $basename ]->slug )
3206 ) {
3207 $slug = self::$_plugins_info->response[ $basename ]->slug;
3208 }
3209 }
3210
3211 if ( empty( $slug ) ) {
3212 // Try to find slug from FS data.
3213 $slug = self::find_slug_by_basename( $basename );
3214 }
3215
3216 if ( empty( $slug ) ) {
3217 // Fallback to plugin's folder name.
3218 $slug = dirname( $basename );
3219 }
3220
3221 return $slug;
3222 }
3223
3224 private static $_statics_loaded = false;
3225
3226 /**
3227 * Load static resources.
3228 *
3229 * @author Vova Feldman (@svovaf)
3230 * @since 1.0.1
3231 */
3232 private static function _load_required_static() {
3233 if ( self::$_statics_loaded ) {
3234 return;
3235 }
3236
3237 self::$_static_logger = FS_Logger::get_logger( WP_FS__SLUG, WP_FS__DEBUG_SDK, WP_FS__ECHO_DEBUG_SDK );
3238
3239 self::$_static_logger->entrance();
3240
3241 self::$_accounts = FS_Options::instance( WP_FS__ACCOUNTS_OPTION_NAME, true );
3242
3243 if ( is_multisite() ) {
3244 $has_skipped_migration = (
3245 // 'id_slug_type_path_map' - was never stored on older versions, therefore, not exists on the site level.
3246 null === self::$_accounts->get_option( 'id_slug_type_path_map', null, false ) &&
3247 // 'file_slug_map' stored on the site level, so it was running an SDK version before it was integrated with MS-network.
3248 null !== self::$_accounts->get_option( 'file_slug_map', null, false )
3249 );
3250
3251 /**
3252 * If the file_slug_map exists on the site level but doesn't exist on the
3253 * network level storage, it means that we need to process the storage with migration.
3254 *
3255 * 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.
3256 *
3257 * @author Vova Feldman (@svovaf)
3258 * @since 2.0.0
3259 */
3260 if (
3261 ( $has_skipped_migration && true !== self::$_accounts->get_option( 'ms_migration_complete', false, true ) ) ||
3262 ( null === self::$_accounts->get_option( 'file_slug_map', null, true ) &&
3263 null !== self::$_accounts->get_option( 'file_slug_map', null, false ) )
3264 ) {
3265 self::migrate_options_to_network();
3266 }
3267 }
3268
3269 self::$_global_admin_notices = FS_Admin_Notices::instance( 'global' );
3270
3271 if ( ! WP_FS__DEMO_MODE ) {
3272 add_action( ( fs_is_network_admin() ? 'network_' : '' ) . 'admin_menu', array(
3273 'Freemius',
3274 '_add_debug_section'
3275 ) );
3276 }
3277
3278 add_action( "wp_ajax_fs_toggle_debug_mode", array( 'Freemius', '_toggle_debug_mode' ) );
3279
3280 self::add_ajax_action_static( 'get_debug_log', array( 'Freemius', '_get_debug_log' ) );
3281
3282 self::add_ajax_action_static( 'get_db_option', array( 'Freemius', '_get_db_option' ) );
3283
3284 self::add_ajax_action_static( 'set_db_option', array( 'Freemius', '_set_db_option' ) );
3285
3286 if ( 0 == did_action( 'plugins_loaded' ) ) {
3287 add_action( 'plugins_loaded', array( 'Freemius', '_load_textdomain' ), 1 );
3288 }
3289
3290 add_action( 'admin_footer', array( 'Freemius', '_enrich_ajax_url' ) );
3291 add_action( 'admin_footer', array( 'Freemius', '_open_support_forum_in_new_page' ) );
3292
3293 if ( self::is_plugins_page() || self::is_themes_page() ) {
3294 add_action( 'admin_print_footer_scripts', array( 'Freemius', '_maybe_add_beta_label_styles' ), 9 );
3295
3296 /**
3297 * Specifically use this hook so that the JS event handlers will work properly on the "Themes"
3298 * page.
3299 *
3300 * @author Leo Fajardo (@leorw)
3301 * @since 2.3.0
3302 */
3303 add_action( 'admin_footer-' . self::get_current_page(), array( 'Freemius', '_maybe_add_beta_label_to_plugins_and_handle_confirmation') );
3304 }
3305
3306 self::$_statics_loaded = true;
3307 }
3308
3309 /**
3310 * @author Leo Fajardo (@leorw)
3311 *
3312 * @since 2.1.3
3313 */
3314 private static function migrate_options_to_network() {
3315 self::migrate_accounts_to_network();
3316
3317 // Migrate API options from site level to network level.
3318 $api_network_options = FS_Option_Manager::get_manager( WP_FS__OPTIONS_OPTION_NAME, true, true );
3319 $api_network_options->migrate_to_network();
3320
3321 // Migrate API cache to network level storage.
3322 FS_Cache_Manager::get_manager( WP_FS__API_CACHE_OPTION_NAME )->migrate_to_network();
3323
3324 self::$_accounts->set_option( 'ms_migration_complete', true, true );
3325 }
3326
3327 #----------------------------------------------------------------------------------
3328 #region Localization
3329 #----------------------------------------------------------------------------------
3330
3331 /**
3332 * Load framework's text domain.
3333 *
3334 * @author Vova Feldman (@svovaf)
3335 * @since 1.2.1
3336 */
3337 static function _load_textdomain() {
3338 if ( ! is_admin() ) {
3339 return;
3340 }
3341
3342 global $fs_active_plugins;
3343
3344 // Works both for plugins and themes.
3345 load_plugin_textdomain(
3346 'freemius',
3347 false,
3348 $fs_active_plugins->newest->sdk_path . '/languages/'
3349 );
3350 }
3351
3352 #endregion
3353
3354 #----------------------------------------------------------------------------------
3355 #region Debugging
3356 #----------------------------------------------------------------------------------
3357
3358 /**
3359 * @author Vova Feldman (@svovaf)
3360 * @since 1.0.8
3361 */
3362 static function _add_debug_section() {
3363 if ( ! is_super_admin() ) {
3364 // Add debug page only for super-admins.
3365 return;
3366 }
3367
3368 self::$_static_logger->entrance();
3369
3370 $title = sprintf( '%s [v.%s]', fs_text_inline( 'Freemius Debug' ), WP_FS__SDK_VERSION );
3371
3372 if ( WP_FS__DEV_MODE ) {
3373 // Add top-level debug menu item.
3374 $hook = FS_Admin_Menu_Manager::add_page(
3375 $title,
3376 $title,
3377 'manage_options',
3378 'freemius',
3379 array( 'Freemius', '_debug_page_render' )
3380 );
3381 } else {
3382 // Add hidden debug page.
3383 $hook = FS_Admin_Menu_Manager::add_subpage(
3384 null,
3385 $title,
3386 $title,
3387 'manage_options',
3388 'freemius',
3389 array( 'Freemius', '_debug_page_render' )
3390 );
3391 }
3392
3393 if ( ! empty( $hook ) ) {
3394 add_action( "load-$hook", array( 'Freemius', '_debug_page_actions' ) );
3395 }
3396 }
3397
3398 /**
3399 * @author Vova Feldman (@svovaf)
3400 * @since 1.1.7.3
3401 */
3402 static function _toggle_debug_mode() {
3403 if ( ! is_super_admin() ) {
3404 return;
3405 }
3406
3407 $is_on = fs_request_get( 'is_on', false, 'post' );
3408
3409 if ( fs_request_is_post() && in_array( $is_on, array( 0, 1 ) ) ) {
3410 update_option( 'fs_debug_mode', $is_on );
3411
3412 // Turn on/off storage logging.
3413 FS_Logger::_set_storage_logging( ( 1 == $is_on ) );
3414 }
3415
3416 exit;
3417 }
3418
3419 /**
3420 * @author Vova Feldman (@svovaf)
3421 * @since 1.2.1.6
3422 */
3423 static function _get_debug_log() {
3424 $logs = FS_Logger::load_db_logs(
3425 fs_request_get( 'filters', false, 'post' ),
3426 ! empty( $_POST['limit'] ) && is_numeric( $_POST['limit'] ) ? $_POST['limit'] : 200,
3427 ! empty( $_POST['offset'] ) && is_numeric( $_POST['offset'] ) ? $_POST['offset'] : 0
3428 );
3429
3430 self::shoot_ajax_success( $logs );
3431 }
3432
3433 /**
3434 * @author Vova Feldman (@svovaf)
3435 * @since 1.2.1.7
3436 */
3437 static function _get_db_option() {
3438 check_admin_referer( 'fs_get_db_option' );
3439
3440 $option_name = fs_request_get( 'option_name' );
3441
3442 if ( ! is_super_admin() ||
3443 ! fs_starts_with( $option_name, 'fs_' )
3444 ) {
3445 self::shoot_ajax_failure();
3446 }
3447
3448 $value = get_option( $option_name );
3449
3450 $result = array(
3451 'name' => $option_name,
3452 );
3453
3454 if ( false !== $value ) {
3455 if ( ! is_string( $value ) ) {
3456 $value = json_encode( $value );
3457 }
3458
3459 $result['value'] = $value;
3460 }
3461
3462 self::shoot_ajax_success( $result );
3463 }
3464
3465 /**
3466 * @author Vova Feldman (@svovaf)
3467 * @since 1.2.1.7
3468 */
3469 static function _set_db_option() {
3470 check_admin_referer( 'fs_set_db_option' );
3471
3472 $option_name = fs_request_get( 'option_name' );
3473
3474 if ( ! is_super_admin() ||
3475 ! fs_starts_with( $option_name, 'fs_' )
3476 ) {
3477 self::shoot_ajax_failure();
3478 }
3479
3480 $option_value = fs_request_get( 'option_value' );
3481
3482 if ( ! empty( $option_value ) ) {
3483 update_option( $option_name, $option_value );
3484 }
3485
3486 self::shoot_ajax_success();
3487 }
3488
3489 /**
3490 * @author Vova Feldman (@svovaf)
3491 * @since 1.0.8
3492 */
3493 static function _debug_page_actions() {
3494 self::_clean_admin_content_section();
3495
3496 if ( fs_request_is_action( 'restart_freemius' ) ) {
3497 check_admin_referer( 'restart_freemius' );
3498
3499 if ( ! is_multisite() ) {
3500 // Clear accounts data.
3501 self::$_accounts->clear( null, true );
3502 } else {
3503 $sites = self::get_sites();
3504 foreach ( $sites as $site ) {
3505 $blog_id = self::get_site_blog_id( $site );
3506 self::$_accounts->clear( $blog_id, true );
3507 }
3508
3509 // Clear network level storage.
3510 self::$_accounts->clear( true, true );
3511 }
3512
3513 // Clear SDK reference cache.
3514 delete_option( 'fs_active_plugins' );
3515 } else if ( fs_request_is_action( 'clear_updates_data' ) ) {
3516 check_admin_referer( 'clear_updates_data' );
3517
3518 if ( ! is_multisite() ) {
3519 set_site_transient( 'update_plugins', null );
3520 set_site_transient( 'update_themes', null );
3521 } else {
3522 $current_blog_id = get_current_blog_id();
3523
3524 $sites = self::get_sites();
3525 foreach ( $sites as $site ) {
3526 switch_to_blog( self::get_site_blog_id( $site ) );
3527
3528 set_site_transient( 'update_plugins', null );
3529 set_site_transient( 'update_themes', null );
3530 }
3531
3532 switch_to_blog( $current_blog_id );
3533 }
3534 } else if ( fs_request_is_action( 'simulate_trial' ) ) {
3535 check_admin_referer( 'simulate_trial' );
3536
3537 $fs = freemius( fs_request_get( 'module_id' ) );
3538
3539 // Update SDK install to at least 24 hours before.
3540 $fs->_storage->install_timestamp = ( time() - WP_FS__TIME_24_HOURS_IN_SEC );
3541 // Unset the trial shown timestamp.
3542 unset( $fs->_storage->trial_promotion_shown );
3543 } else if ( fs_request_is_action( 'simulate_network_upgrade' ) ) {
3544 check_admin_referer( 'simulate_network_upgrade' );
3545
3546 $fs = freemius( fs_request_get( 'module_id' ) );
3547
3548 self::set_network_upgrade_mode( $fs->_storage );
3549 } else if ( fs_request_is_action( 'delete_install' ) ) {
3550 check_admin_referer( 'delete_install' );
3551
3552 self::_delete_site_by_slug(
3553 fs_request_get( 'slug' ),
3554 fs_request_get( 'module_type' ),
3555 true,
3556 fs_request_get( 'blog_id', null )
3557 );
3558 } else if ( fs_request_is_action( 'delete_user' ) ) {
3559 check_admin_referer( 'delete_user' );
3560
3561 self::delete_user( fs_request_get( 'user_id' ) );
3562 } else if ( fs_request_is_action( 'download_logs' ) ) {
3563 check_admin_referer( 'download_logs' );
3564
3565 $download_url = FS_Logger::download_db_logs(
3566 fs_request_get( 'filters', false, 'post' )
3567 );
3568
3569 if ( false === $download_url ) {
3570 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.' );
3571 }
3572
3573 fs_redirect( $download_url );
3574 } else if ( fs_request_is_action( 'migrate_options_to_network' ) ) {
3575 check_admin_referer( 'migrate_options_to_network' );
3576
3577 self::migrate_options_to_network();
3578 }
3579 }
3580
3581 /**
3582 * @author Vova Feldman (@svovaf)
3583 * @since 1.0.8
3584 */
3585 static function _debug_page_render() {
3586 self::$_static_logger->entrance();
3587
3588 if ( ! is_multisite() ) {
3589 $all_plugins_installs = self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN );
3590 $all_themes_installs = self::get_all_sites( WP_FS__MODULE_TYPE_THEME );
3591 } else {
3592 $sites = self::get_sites();
3593
3594 $all_plugins_installs = array();
3595 $all_themes_installs = array();
3596
3597 foreach ( $sites as $site ) {
3598 $blog_id = self::get_site_blog_id( $site );
3599
3600 $plugins_installs = self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN, $blog_id );
3601
3602 foreach ( $plugins_installs as $slug => $install ) {
3603 if ( ! isset( $all_plugins_installs[ $slug ] ) ) {
3604 $all_plugins_installs[ $slug ] = array();
3605 }
3606
3607 $install->blog_id = $blog_id;
3608
3609 $all_plugins_installs[ $slug ][] = $install;
3610 }
3611
3612 $themes_installs = self::get_all_sites( WP_FS__MODULE_TYPE_THEME, $blog_id );
3613
3614 foreach ( $themes_installs as $slug => $install ) {
3615 if ( ! isset( $all_themes_installs[ $slug ] ) ) {
3616 $all_themes_installs[ $slug ] = array();
3617 }
3618
3619 $install->blog_id = $blog_id;
3620
3621 $all_themes_installs[ $slug ][] = $install;
3622 }
3623 }
3624 }
3625
3626 $licenses_by_module_type = self::get_all_licenses_by_module_type();
3627
3628 $vars = array(
3629 'plugin_sites' => $all_plugins_installs,
3630 'theme_sites' => $all_themes_installs,
3631 'users' => self::get_all_users(),
3632 'addons' => self::get_all_addons(),
3633 'account_addons' => self::get_all_account_addons(),
3634 'plugin_licenses' => $licenses_by_module_type[ WP_FS__MODULE_TYPE_PLUGIN ],
3635 'theme_licenses' => $licenses_by_module_type[ WP_FS__MODULE_TYPE_THEME ]
3636 );
3637
3638 fs_enqueue_local_style( 'fs_debug', '/admin/debug.css' );
3639 fs_require_once_template( 'debug.php', $vars );
3640 }
3641
3642 #endregion
3643
3644 #----------------------------------------------------------------------------------
3645 #region Connectivity Issues
3646 #----------------------------------------------------------------------------------
3647
3648 /**
3649 * Check if Freemius should be turned on for the current plugin install.
3650 *
3651 * Note:
3652 * $this->_is_on is updated in has_api_connectivity()
3653 *
3654 * @author Vova Feldman (@svovaf)
3655 * @since 1.0.9
3656 *
3657 * @return bool
3658 */
3659 function is_on() {
3660 self::$_static_logger->entrance();
3661
3662 if ( isset( $this->_is_on ) ) {
3663 return $this->_is_on;
3664 }
3665
3666 // If already installed or pending then sure it's on :)
3667 if ( $this->is_registered() || $this->is_pending_activation() ) {
3668 $this->_is_on = true;
3669
3670 return true;
3671 }
3672
3673 return false;
3674 }
3675
3676 /**
3677 * @author Vova Feldman (@svovaf)
3678 * @since 1.1.7.3
3679 *
3680 * @param bool $flush_if_no_connectivity
3681 *
3682 * @return bool
3683 */
3684 private function should_run_connectivity_test( $flush_if_no_connectivity = false ) {
3685 if ( ! isset( $this->_storage->connectivity_test ) ) {
3686 // Connectivity test was never executed, or cache was cleared.
3687 return true;
3688 }
3689
3690 if ( WP_FS__PING_API_ON_IP_OR_HOST_CHANGES ) {
3691 if ( WP_FS__IS_HTTP_REQUEST ) {
3692 if ( $_SERVER['HTTP_HOST'] != $this->_storage->connectivity_test['host'] ) {
3693 // Domain changed.
3694 return true;
3695 }
3696
3697 if ( WP_FS__REMOTE_ADDR != $this->_storage->connectivity_test['server_ip'] ) {
3698 // Server IP changed.
3699 return true;
3700 }
3701 }
3702 }
3703
3704 if ( $this->_storage->connectivity_test['is_connected'] &&
3705 $this->_storage->connectivity_test['is_active']
3706 ) {
3707 // API connected and Freemius is active - no need to run connectivity check.
3708 return false;
3709 }
3710
3711 if ( $flush_if_no_connectivity ) {
3712 /**
3713 * If explicitly asked to flush when no connectivity - do it only
3714 * if at least 10 sec passed from the last API connectivity test.
3715 */
3716 return ( isset( $this->_storage->connectivity_test['timestamp'] ) &&
3717 ( WP_FS__SCRIPT_START_TIME - $this->_storage->connectivity_test['timestamp'] ) > 10 );
3718 }
3719
3720 /**
3721 * @since 1.1.7 Don't check for connectivity on plugin downgrade.
3722 */
3723 $version = $this->get_plugin_version();
3724 if ( version_compare( $version, $this->_storage->connectivity_test['version'], '>' ) ) {
3725 // If it's a plugin version upgrade and Freemius is off or no connectivity, run connectivity test.
3726 return true;
3727 }
3728
3729 return false;
3730 }
3731
3732 /**
3733 * @author Vova Feldman (@svovaf)
3734 * @since 1.1.7.4
3735 *
3736 * @param int|null $blog_id Since 2.0.0.
3737 * @param bool $is_gdpr_test Since 2.0.2. Perform only the GDPR test.
3738 *
3739 * @return object|false
3740 */
3741 private function ping( $blog_id = null, $is_gdpr_test = false ) {
3742 if ( WP_FS__SIMULATE_NO_API_CONNECTIVITY ) {
3743 return false;
3744 }
3745
3746 $version = $this->get_plugin_version();
3747
3748 $is_update = $this->apply_filters( 'is_plugin_update', $this->is_plugin_update() );
3749
3750 return $this->get_api_plugin_scope()->ping(
3751 $this->get_anonymous_id( $blog_id ),
3752 array(
3753 'is_update' => json_encode( $is_update ),
3754 'version' => $version,
3755 'sdk' => $this->version,
3756 'is_admin' => json_encode( is_admin() ),
3757 'is_ajax' => json_encode( self::is_ajax() ),
3758 'is_cron' => json_encode( self::is_cron() ),
3759 'is_gdpr_test' => $is_gdpr_test,
3760 'is_http' => json_encode( WP_FS__IS_HTTP_REQUEST ),
3761 )
3762 );
3763 }
3764
3765 /**
3766 * Check if there's any connectivity issue to Freemius API.
3767 *
3768 * @author Vova Feldman (@svovaf)
3769 * @since 1.0.9
3770 *
3771 * @param bool $flush_if_no_connectivity
3772 *
3773 * @return bool
3774 */
3775 function has_api_connectivity( $flush_if_no_connectivity = false ) {
3776 $this->_logger->entrance();
3777
3778 if ( isset( $this->_has_api_connection ) && ( $this->_has_api_connection || ! $flush_if_no_connectivity ) ) {
3779 return $this->_has_api_connection;
3780 }
3781
3782 if ( WP_FS__SIMULATE_NO_API_CONNECTIVITY &&
3783 isset( $this->_storage->connectivity_test ) &&
3784 true === $this->_storage->connectivity_test['is_connected']
3785 ) {
3786 unset( $this->_storage->connectivity_test );
3787 }
3788
3789 if ( ! $this->should_run_connectivity_test( $flush_if_no_connectivity ) ) {
3790 $this->_has_api_connection = $this->_storage->connectivity_test['is_connected'];
3791 /**
3792 * @since 1.1.6 During dev mode, if there's connectivity - turn Freemius on regardless the configuration.
3793 *
3794 * @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.
3795 */
3796 $this->_is_on = $this->_storage->connectivity_test['is_active'] ||
3797 $this->is_premium() ||
3798 ( WP_FS__DEV_MODE && $this->_has_api_connection && ! WP_FS__SIMULATE_FREEMIUS_OFF );
3799
3800 return $this->_has_api_connection;
3801 }
3802
3803 $pong = $this->ping();
3804 $is_connected = $this->get_api_plugin_scope()->is_valid_ping( $pong );
3805
3806 if ( ! $is_connected ) {
3807 // API failure.
3808 $this->_add_connectivity_issue_message( $pong );
3809 }
3810
3811 if ( $is_connected ) {
3812 FS_GDPR_Manager::instance()->store_is_required( $pong->is_gdpr_required );
3813 }
3814
3815 $this->store_connectivity_info( $pong, $is_connected );
3816
3817 return $this->_has_api_connection;
3818 }
3819
3820 /**
3821 * @author Vova Feldman (@svovaf)
3822 * @since 1.1.7.4
3823 *
3824 * @param object $pong
3825 * @param bool $is_connected
3826 */
3827 private function store_connectivity_info( $pong, $is_connected ) {
3828 $this->_logger->entrance();
3829
3830 $version = $this->get_plugin_version();
3831
3832 if ( ! $is_connected || WP_FS__SIMULATE_FREEMIUS_OFF ) {
3833 $is_active = false;
3834 } else {
3835 $is_active = ( isset( $pong->is_active ) && true == $pong->is_active );
3836 }
3837
3838 $is_active = $this->apply_filters(
3839 'is_on',
3840 $is_active,
3841 $this->is_plugin_update(),
3842 $version
3843 );
3844
3845 $this->_storage->connectivity_test = array(
3846 'is_connected' => $is_connected,
3847 'host' => $_SERVER['HTTP_HOST'],
3848 'server_ip' => WP_FS__REMOTE_ADDR,
3849 'is_active' => $is_active,
3850 'timestamp' => WP_FS__SCRIPT_START_TIME,
3851 // Last version with connectivity attempt.
3852 'version' => $version,
3853 );
3854
3855 $this->_has_api_connection = $is_connected;
3856 $this->_is_on = $is_active || ( WP_FS__DEV_MODE && $is_connected && ! WP_FS__SIMULATE_FREEMIUS_OFF );
3857 }
3858
3859 /**
3860 * Force turning Freemius on.
3861 *
3862 * @author Vova Feldman (@svovaf)
3863 * @since 1.1.8.1
3864 *
3865 * @return bool TRUE if successfully turned on.
3866 */
3867 private function turn_on() {
3868 $this->_logger->entrance();
3869
3870 if ( $this->is_on() || ! isset( $this->_storage->connectivity_test['is_active'] ) ) {
3871 return false;
3872 }
3873
3874 $updated_connectivity = $this->_storage->connectivity_test;
3875 $updated_connectivity['is_active'] = true;
3876 $updated_connectivity['timestamp'] = WP_FS__SCRIPT_START_TIME;
3877 $this->_storage->connectivity_test = $updated_connectivity;
3878
3879 $this->_is_on = true;
3880
3881 return true;
3882 }
3883
3884 /**
3885 * Anonymous and unique site identifier (Hash).
3886 *
3887 * @author Vova Feldman (@svovaf)
3888 * @since 1.1.0
3889 *
3890 * @param null|int $blog_id Since 2.0.0
3891 *
3892 * @return string
3893 */
3894 function get_anonymous_id( $blog_id = null ) {
3895 $unique_id = self::$_accounts->get_option( 'unique_id', null, $blog_id );
3896
3897 if ( empty( $unique_id ) || ! is_string( $unique_id ) ) {
3898 $key = fs_strip_url_protocol( get_site_url( $blog_id ) );
3899
3900 $secure_auth = SECURE_AUTH_KEY;
3901 if ( empty( $secure_auth ) ||
3902 false !== strpos( $secure_auth, ' ' ) ||
3903 'put your unique phrase here' === $secure_auth
3904 ) {
3905 // Protect against default auth key.
3906 $secure_auth = md5( microtime() );
3907 }
3908
3909 /**
3910 * Base the unique identifier on the WP secure authentication key. Which
3911 * turns the key into a secret anonymous identifier. This will help us
3912 * to avoid duplicate installs generation on the backend upon opt-in.
3913 *
3914 * @author Vova Feldman (@svovaf)
3915 * @since 1.2.3
3916 */
3917 $unique_id = md5( $key . $secure_auth );
3918
3919 self::$_accounts->set_option( 'unique_id', $unique_id, true, $blog_id );
3920 }
3921
3922 $this->_logger->departure( $unique_id );
3923
3924 return $unique_id;
3925 }
3926
3927 /**
3928 * @author Vova Feldman (@svovaf)
3929 * @since 1.1.7.4
3930 *
3931 * @return \WP_User
3932 */
3933 static function _get_current_wp_user() {
3934 self::require_pluggable_essentials();
3935 self::wp_cookie_constants();
3936
3937 return wp_get_current_user();
3938 }
3939
3940 /**
3941 * Define cookie constants which are required by Freemius::_get_current_wp_user() since
3942 * it uses wp_get_current_user() which needs the cookie constants set. When a plugin
3943 * is network activated the cookie constants are only configured after the network
3944 * plugins activation, therefore, if we don't define those constants WP will throw
3945 * PHP warnings/notices.
3946 *
3947 * @author Vova Feldman (@svovaf)
3948 * @since 2.1.1
3949 */
3950 private static function wp_cookie_constants() {
3951 if ( defined( 'LOGGED_IN_COOKIE' ) &&
3952 ( defined( 'AUTH_COOKIE' ) || defined( 'SECURE_AUTH_COOKIE' ) )
3953 ) {
3954 return;
3955 }
3956
3957 /**
3958 * Used to guarantee unique hash cookies
3959 *
3960 * @since 1.5.0
3961 */
3962 if ( ! defined( 'COOKIEHASH' ) ) {
3963 $siteurl = get_site_option( 'siteurl' );
3964 if ( $siteurl ) {
3965 define( 'COOKIEHASH', md5( $siteurl ) );
3966 } else {
3967 define( 'COOKIEHASH', '' );
3968 }
3969 }
3970
3971 if ( ! defined( 'LOGGED_IN_COOKIE' ) ) {
3972 define( 'LOGGED_IN_COOKIE', 'wordpress_logged_in_' . COOKIEHASH );
3973 }
3974
3975 /**
3976 * @since 2.5.0
3977 */
3978 if ( ! defined( 'AUTH_COOKIE' ) ) {
3979 define( 'AUTH_COOKIE', 'wordpress_' . COOKIEHASH );
3980 }
3981
3982 /**
3983 * @since 2.6.0
3984 */
3985 if ( ! defined( 'SECURE_AUTH_COOKIE' ) ) {
3986 define( 'SECURE_AUTH_COOKIE', 'wordpress_sec_' . COOKIEHASH );
3987 }
3988 }
3989
3990 /**
3991 * @author Vova Feldman (@svovaf)
3992 * @since 2.1.0
3993 *
3994 * @return int
3995 */
3996 static function get_current_wp_user_id() {
3997 $wp_user = self::_get_current_wp_user();
3998
3999 return $wp_user->ID;
4000 }
4001
4002 /**
4003 * @author Vova Feldman (@svovaf)
4004 * @since 1.2.1.7
4005 *
4006 * @param string $email
4007 *
4008 * @return bool
4009 */
4010 static function is_valid_email( $email ) {
4011 if ( false === filter_var( $email, FILTER_VALIDATE_EMAIL ) ) {
4012 return false;
4013 }
4014
4015 $parts = explode( '@', $email );
4016
4017 if ( 2 !== count( $parts ) || empty( $parts[1] ) ) {
4018 return false;
4019 }
4020
4021 $blacklist = array(
4022 'admin.',
4023 'webmaster.',
4024 'localhost.',
4025 'dev.',
4026 'development.',
4027 'test.',
4028 'stage.',
4029 'staging.',
4030 );
4031
4032 // Make sure domain is not one of the blacklisted.
4033 foreach ( $blacklist as $invalid ) {
4034 if ( 0 === strpos( $parts[1], $invalid ) ) {
4035 return false;
4036 }
4037 }
4038
4039 // Get the UTF encoded domain name.
4040 $domain = idn_to_ascii( $parts[1] ) . '.';
4041
4042 return ( checkdnsrr( $domain, 'MX' ) || checkdnsrr( $domain, 'A' ) );
4043 }
4044
4045 /**
4046 * Generate API connectivity issue message.
4047 *
4048 * @author Vova Feldman (@svovaf)
4049 * @since 1.0.9
4050 *
4051 * @param mixed $api_result
4052 * @param bool $is_first_failure
4053 */
4054 function _add_connectivity_issue_message( $api_result, $is_first_failure = true ) {
4055 if ( ! $this->is_premium() && $this->_enable_anonymous ) {
4056 // Don't add message if it's the free version and can run anonymously.
4057 return;
4058 }
4059
4060 if ( ! function_exists( 'wp_nonce_url' ) ) {
4061 require_once ABSPATH . 'wp-includes/functions.php';
4062 }
4063
4064 $current_user = self::_get_current_wp_user();
4065 // $admin_email = get_option( 'admin_email' );
4066 $admin_email = $current_user->user_email;
4067
4068 // Aliases.
4069 $deactivate_plugin_title = $this->esc_html_inline( 'That\'s exhausting, please deactivate', 'deactivate-plugin-title' );
4070 $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' );
4071 $install_previous_title = $this->esc_html_inline( 'Let\'s try your previous version', 'install-previous-title' );
4072 $install_previous_desc = $this->esc_html_inline( 'Uninstall this version and install the previous one.', 'install-previous-desc' );
4073 $fix_issue_title = $this->esc_html_inline( 'Yes - I\'m giving you a chance to fix it', 'fix-issue-title' );
4074 $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' );
4075 /* translators: %s: product title (e.g. "Awesome Plugin" requires an access to...) */
4076 $x_requires_access_to_api = $this->esc_html_inline( '%s requires an access to our API.', 'x-requires-access-to-api' );
4077 $sysadmin_title = $this->esc_html_inline( 'I\'m a system administrator', 'sysadmin-title' );
4078 $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' );
4079
4080 $message = false;
4081 if ( is_object( $api_result ) &&
4082 isset( $api_result->error ) &&
4083 isset( $api_result->error->code )
4084 ) {
4085 switch ( $api_result->error->code ) {
4086 case 'curl_missing':
4087 $missing_methods = '';
4088 if ( is_array( $api_result->missing_methods ) &&
4089 ! empty( $api_result->missing_methods )
4090 ) {
4091 foreach ( $api_result->missing_methods as $m ) {
4092 if ( 'curl_version' === $m ) {
4093 continue;
4094 }
4095
4096 if ( ! empty( $missing_methods ) ) {
4097 $missing_methods .= ', ';
4098 }
4099
4100 $missing_methods .= sprintf( '<code>%s</code>', $m );
4101 }
4102
4103 if ( ! empty( $missing_methods ) ) {
4104 $missing_methods = sprintf(
4105 '<br><br><b>%s</b> %s',
4106 $this->esc_html_inline( 'Disabled method(s):', 'curl-disabled-methods' ),
4107 $missing_methods
4108 );
4109 }
4110 }
4111
4112 $message = sprintf(
4113 $x_requires_access_to_api . ' ' .
4114 $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' ) . ' ' .
4115 $missing_methods .
4116 ' %s',
4117 '<b>' . $this->get_plugin_name() . '</b>',
4118 sprintf(
4119 '<ol id="fs_firewall_issue_options"><li>%s</li><li>%s</li><li>%s</li></ol>',
4120 sprintf(
4121 '<a class="fs-resolve" data-type="curl" href="#"><b>%s</b></a>%s',
4122 $this->get_text_inline( 'I don\'t know what is cURL or how to install it, help me!', 'curl-missing-no-clue-title' ),
4123 ' - ' . sprintf(
4124 $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' ),
4125 '<a href="mailto:' . $admin_email . '">' . $admin_email . '</a>'
4126 )
4127 ),
4128 sprintf(
4129 '<b>%s</b> - %s',
4130 $sysadmin_title,
4131 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 ) ) )
4132 ),
4133 sprintf(
4134 '<a href="%s"><b>%s</b></a> - %s',
4135 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 ),
4136 $deactivate_plugin_title,
4137 $deactivate_plugin_desc
4138 )
4139 )
4140 );
4141 break;
4142 case 'cloudflare_ddos_protection':
4143 $message = sprintf(
4144 $x_requires_access_to_api . ' ' .
4145 $this->esc_html_inline( 'From unknown reason, CloudFlare, the firewall we use, blocks the connection.', 'cloudflare-blocks-connection-message' ) . ' ' .
4146 $happy_to_resolve_issue_asap .
4147 ' %s',
4148 '<b>' . $this->get_plugin_name() . '</b>',
4149 sprintf(
4150 '<ol id="fs_firewall_issue_options"><li>%s</li><li>%s</li><li>%s</li></ol>',
4151 sprintf(
4152 '<a class="fs-resolve" data-type="cloudflare" href="#"><b>%s</b></a>%s',
4153 $fix_issue_title,
4154 ' - ' . sprintf(
4155 $fix_issue_desc,
4156 '<a href="mailto:' . $admin_email . '">' . $admin_email . '</a>'
4157 )
4158 ),
4159 sprintf(
4160 '<a href="%s" target="_blank"><b>%s</b></a> - %s',
4161 sprintf( 'https://wordpress.org/plugins/%s/download/', $this->_slug ),
4162 $install_previous_title,
4163 $install_previous_desc
4164 ),
4165 sprintf(
4166 '<a href="%s"><b>%s</b></a> - %s',
4167 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 ),
4168 $deactivate_plugin_title,
4169 $deactivate_plugin_desc
4170 )
4171 )
4172 );
4173 break;
4174 case 'squid_cache_block':
4175 $message = sprintf(
4176 $x_requires_access_to_api . ' ' .
4177 $this->esc_html_inline( 'It looks like your server is using Squid ACL (access control lists), which blocks the connection.', 'squid-blocks-connection-message' ) .
4178 ' %s',
4179 '<b>' . $this->get_plugin_name() . '</b>',
4180 sprintf(
4181 '<ol id="fs_firewall_issue_options"><li>%s</li><li>%s</li><li>%s</li></ol>',
4182 sprintf(
4183 '<a class="fs-resolve" data-type="squid" href="#"><b>%s</b></a> - %s',
4184 $this->esc_html_inline( 'I don\'t know what is Squid or ACL, help me!', 'squid-no-clue-title' ),
4185 sprintf(
4186 $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' ),
4187 '<a href="mailto:' . $admin_email . '">' . $admin_email . '</a>'
4188 )
4189 ),
4190 sprintf(
4191 '<b>%s</b> - %s',
4192 $sysadmin_title,
4193 sprintf(
4194 $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' ),
4195 // We use a filter since the plugin might require additional API connectivity.
4196 '<b>' . implode( ', ', $this->apply_filters( 'api_domains', array(
4197 'api.freemius.com',
4198 'wp.freemius.com'
4199 ) ) ) . '</b>',
4200 $this->_module_type
4201 )
4202 ),
4203 sprintf(
4204 '<a href="%s"><b>%s</b></a> - %s',
4205 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 ),
4206 $deactivate_plugin_title,
4207 $deactivate_plugin_desc
4208 )
4209 )
4210 );
4211 break;
4212 // default:
4213 // $message = $this->get_text_inline( 'connectivity-test-fails-message' );
4214 // break;
4215 }
4216 }
4217
4218 $message_id = 'failed_connect_api';
4219 $type = 'error';
4220
4221 $connectivity_test_fails_message = $this->esc_html_inline( 'From unknown reason, the API connectivity test failed.', 'connectivity-test-fails-message' );
4222
4223 if ( false === $message ) {
4224 if ( $is_first_failure ) {
4225 // First attempt failed.
4226 $message = sprintf(
4227 $x_requires_access_to_api . ' ' .
4228 $connectivity_test_fails_message . ' ' .
4229 $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>' .
4230 '%s',
4231 '<b>' . $this->get_plugin_name() . '</b>',
4232 sprintf(
4233 '<div id="fs_firewall_issue_options">%s %s</div>',
4234 sprintf(
4235 '<a class="button button-primary fs-resolve" data-type="retry_ping" href="#">%s</a>',
4236 $this->get_text_inline( 'Yes - do your thing', 'yes-do-your-thing' )
4237 ),
4238 sprintf(
4239 '<a href="%s" class="button">%s</a>',
4240 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 ),
4241 $this->get_text_inline( 'No - just deactivate', 'no-deactivate' )
4242 )
4243 )
4244 );
4245
4246 $message_id = 'failed_connect_api_first';
4247 $type = 'promotion';
4248 } else {
4249 // Second connectivity attempt failed.
4250 $message = sprintf(
4251 $x_requires_access_to_api . ' ' .
4252 $connectivity_test_fails_message . ' ' .
4253 $happy_to_resolve_issue_asap .
4254 ' %s',
4255 '<b>' . $this->get_plugin_name() . '</b>',
4256 sprintf(
4257 '<ol id="fs_firewall_issue_options"><li>%s</li><li>%s</li><li>%s</li></ol>',
4258 sprintf(
4259 '<a class="fs-resolve" data-type="general" href="#"><b>%s</b></a>%s',
4260 $fix_issue_title,
4261 ' - ' . sprintf(
4262 $fix_issue_desc,
4263 '<a href="mailto:' . $admin_email . '">' . $admin_email . '</a>'
4264 )
4265 ),
4266 sprintf(
4267 '<a href="%s" target="_blank"><b>%s</b></a> - %s',
4268 sprintf( 'https://wordpress.org/plugins/%s/download/', $this->_slug ),
4269 $install_previous_title,
4270 $install_previous_desc
4271 ),
4272 sprintf(
4273 '<a href="%s"><b>%s</b></a> - %s',
4274 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 ),
4275 $deactivate_plugin_title,
4276 $deactivate_plugin_desc
4277 )
4278 )
4279 );
4280 }
4281 }
4282
4283 $this->_admin_notices->add_sticky(
4284 $message,
4285 $message_id,
4286 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
4287 $type
4288 );
4289 }
4290
4291 /**
4292 * Handle user request to resolve connectivity issue.
4293 * This method will send an email to Freemius API technical staff for resolution.
4294 * The email will contain server's info and installed plugins (might be caching issue).
4295 *
4296 * @author Vova Feldman (@svovaf)
4297 * @since 1.0.9
4298 */
4299 function _email_about_firewall_issue() {
4300 $this->_admin_notices->remove_sticky( 'failed_connect_api' );
4301
4302 $pong = $this->ping();
4303
4304 $is_connected = $this->get_api_plugin_scope()->is_valid_ping( $pong );
4305
4306 if ( $is_connected ) {
4307 FS_GDPR_Manager::instance()->store_is_required( $pong->is_gdpr_required );
4308
4309 $this->store_connectivity_info( $pong, $is_connected );
4310
4311 echo $this->get_after_plugin_activation_redirect_url();
4312 exit;
4313 }
4314
4315 $current_user = self::_get_current_wp_user();
4316 $admin_email = $current_user->user_email;
4317
4318 $error_type = fs_request_get( 'error_type', 'general' );
4319
4320 switch ( $error_type ) {
4321 case 'squid':
4322 $title = 'Squid ACL Blocking Issue';
4323 break;
4324 case 'cloudflare':
4325 $title = 'CloudFlare Blocking Issue';
4326 break;
4327 default:
4328 $title = 'API Connectivity Issue';
4329 break;
4330 }
4331
4332 $custom_email_sections = array();
4333
4334 // Add 'API Error' custom email section.
4335 $custom_email_sections['api_error'] = array(
4336 'title' => 'API Error',
4337 'rows' => array(
4338 'ping' => array(
4339 'API Error',
4340 is_string( $pong ) ? htmlentities( $pong ) : json_encode( $pong )
4341 ),
4342 )
4343 );
4344
4345 // Send email with technical details to resolve API connectivity issues.
4346 $this->send_email(
4347 'api@freemius.com', // recipient
4348 $title . ' [' . $this->get_plugin_name() . ']', // subject
4349 $custom_email_sections,
4350 array( "Reply-To: $admin_email <$admin_email>" ) // headers
4351 );
4352
4353 $this->_admin_notices->add_sticky(
4354 sprintf(
4355 $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' ),
4356 '<a href="mailto:' . $admin_email . '">' . $admin_email . '</a>'
4357 ),
4358 'server_details_sent'
4359 );
4360
4361 // Action was taken, tell that API connectivity troubleshooting should be off now.
4362
4363 echo "1";
4364 exit;
4365 }
4366
4367 /**
4368 * Handle connectivity test retry approved by the user.
4369 *
4370 * @author Vova Feldman (@svovaf)
4371 * @since 1.1.7.4
4372 */
4373 function _retry_connectivity_test() {
4374 $this->_admin_notices->remove_sticky( 'failed_connect_api_first' );
4375
4376 $pong = $this->ping();
4377
4378 $is_connected = $this->get_api_plugin_scope()->is_valid_ping( $pong );
4379
4380 if ( $is_connected ) {
4381 FS_GDPR_Manager::instance()->store_is_required( $pong->is_gdpr_required );
4382
4383 $this->store_connectivity_info( $pong, $is_connected );
4384
4385 echo $this->get_after_plugin_activation_redirect_url();
4386 } else {
4387 // Add connectivity issue message after 2nd failed attempt.
4388 $this->_add_connectivity_issue_message( $pong, false );
4389
4390 echo "1";
4391 }
4392
4393 exit;
4394 }
4395
4396 static function _add_firewall_issues_javascript() {
4397 $params = array();
4398 fs_require_once_template( 'firewall-issues-js.php', $params );
4399 }
4400
4401 #endregion
4402
4403 #----------------------------------------------------------------------------------
4404 #region Email
4405 #----------------------------------------------------------------------------------
4406
4407 /**
4408 * Generates and sends an HTML email with customizable sections.
4409 *
4410 * @author Leo Fajardo (@leorw)
4411 * @since 1.1.2
4412 *
4413 * @param string $to_address
4414 * @param string $subject
4415 * @param array $sections
4416 * @param array $headers
4417 *
4418 * @return bool Whether the email contents were sent successfully.
4419 */
4420 private function send_email(
4421 $to_address,
4422 $subject,
4423 $sections = array(),
4424 $headers = array()
4425 ) {
4426 $default_sections = $this->get_email_sections();
4427
4428 // Insert new sections or replace the default email sections.
4429 if ( is_array( $sections ) && ! empty( $sections ) ) {
4430 foreach ( $sections as $section_id => $custom_section ) {
4431 if ( ! isset( $default_sections[ $section_id ] ) ) {
4432 // If the section does not exist, add it.
4433 $default_sections[ $section_id ] = $custom_section;
4434 } else {
4435 // If the section already exists, override it.
4436 $current_section = $default_sections[ $section_id ];
4437
4438 // Replace the current section's title if a custom section title exists.
4439 if ( isset( $custom_section['title'] ) ) {
4440 $current_section['title'] = $custom_section['title'];
4441 }
4442
4443 // Insert new rows under the current section or replace the default rows.
4444 if ( isset( $custom_section['rows'] ) && is_array( $custom_section['rows'] ) && ! empty( $custom_section['rows'] ) ) {
4445 foreach ( $custom_section['rows'] as $row_id => $row ) {
4446 $current_section['rows'][ $row_id ] = $row;
4447 }
4448 }
4449
4450 $default_sections[ $section_id ] = $current_section;
4451 }
4452 }
4453 }
4454
4455 $vars = array( 'sections' => $default_sections );
4456 $message = fs_get_template( 'email.php', $vars );
4457
4458 // Set the type of email to HTML.
4459 $headers[] = 'Content-type: text/html; charset=UTF-8';
4460
4461 $header_string = implode( "\r\n", $headers );
4462
4463 return wp_mail(
4464 $to_address,
4465 $subject,
4466 $message,
4467 $header_string
4468 );
4469 }
4470
4471 /**
4472 * Generates the data for the sections of the email content.
4473 *
4474 * @author Leo Fajardo (@leorw)
4475 * @since 1.1.2
4476 *
4477 * @return array
4478 */
4479 private function get_email_sections() {
4480 // Retrieve the current user's information so that we can get the user's email, first name, and last name below.
4481 $current_user = self::_get_current_wp_user();
4482
4483 // Retrieve the cURL version information so that we can get the version number below.
4484 $curl_version_information = curl_version();
4485
4486 $active_plugin = self::get_active_plugins();
4487
4488 // Generate the list of active plugins separated by new line.
4489 $active_plugin_string = '';
4490 foreach ( $active_plugin as $plugin ) {
4491 $active_plugin_string .= sprintf(
4492 '<a href="%s">%s</a> [v%s]<br>',
4493 $plugin['PluginURI'],
4494 $plugin['Name'],
4495 $plugin['Version']
4496 );
4497 }
4498
4499 $server_ip = WP_FS__REMOTE_ADDR;
4500
4501 // Add PHP info for deeper investigation.
4502 ob_start();
4503 phpinfo();
4504 $php_info = ob_get_clean();
4505
4506 $api_domain = substr( FS_API__ADDRESS, strpos( FS_API__ADDRESS, ':' ) + 3 );
4507
4508 // Generate the default email sections.
4509 $sections = array(
4510 'sdk' => array(
4511 'title' => 'SDK',
4512 'rows' => array(
4513 'fs_version' => array( 'FS Version', $this->version ),
4514 'curl_version' => array( 'cURL Version', $curl_version_information['version'] )
4515 )
4516 ),
4517 'plugin' => array(
4518 'title' => ucfirst( $this->get_module_type() ),
4519 'rows' => array(
4520 'name' => array( 'Name', $this->get_plugin_name() ),
4521 'version' => array( 'Version', $this->get_plugin_version() )
4522 )
4523 ),
4524 'api' => array(
4525 'title' => 'API Subdomain',
4526 'rows' => array(
4527 'dns' => array(
4528 'DNS_CNAME',
4529 function_exists( 'dns_get_record' ) ?
4530 var_export( dns_get_record( $api_domain, DNS_CNAME ), true ) :
4531 'dns_get_record() disabled/blocked'
4532 ),
4533 'ip' => array(
4534 'IP',
4535 function_exists( 'gethostbyname' ) ?
4536 gethostbyname( $api_domain ) :
4537 'gethostbyname() disabled/blocked'
4538 ),
4539 ),
4540 ),
4541 'site' => array(
4542 'title' => 'Site',
4543 'rows' => array(
4544 'unique_id' => array( 'Unique ID', $this->get_anonymous_id() ),
4545 'address' => array( 'Address', site_url() ),
4546 'host' => array(
4547 'HTTP_HOST',
4548 ( ! empty( $_SERVER['HTTP_HOST'] ) ? $_SERVER['HTTP_HOST'] : '' )
4549 ),
4550 'hosting' => array(
4551 'Hosting Company' => fs_request_has( 'hosting_company' ) ?
4552 fs_request_get( 'hosting_company' ) :
4553 'Unknown',
4554 ),
4555 'server_addr' => array(
4556 'SERVER_ADDR',
4557 '<a href="http://www.projecthoneypot.org/ip_' . $server_ip . '">' . $server_ip . '</a>'
4558 )
4559 )
4560 ),
4561 'user' => array(
4562 'title' => 'User',
4563 'rows' => array(
4564 'email' => array( 'Email', $current_user->user_email ),
4565 'first' => array( 'First', $current_user->user_firstname ),
4566 'last' => array( 'Last', $current_user->user_lastname )
4567 )
4568 ),
4569 'plugins' => array(
4570 'title' => 'Plugins',
4571 'rows' => array(
4572 'active_plugins' => array( 'Active Plugins', $active_plugin_string )
4573 )
4574 ),
4575 'php_info' => array(
4576 'title' => 'PHP Info',
4577 'rows' => array(
4578 'info' => array( $php_info )
4579 ),
4580 )
4581 );
4582
4583 // Allow the sections to be modified by other code.
4584 $sections = $this->apply_filters( 'email_template_sections', $sections );
4585
4586 return $sections;
4587 }
4588
4589 #endregion
4590
4591 #----------------------------------------------------------------------------------
4592 #region Initialization
4593 #----------------------------------------------------------------------------------
4594
4595 /**
4596 * Init plugin's Freemius instance.
4597 *
4598 * @author Vova Feldman (@svovaf)
4599 * @since 1.0.1
4600 *
4601 * @param number $id
4602 * @param string $public_key
4603 * @param bool $is_live
4604 * @param bool $is_premium
4605 */
4606 function init( $id, $public_key, $is_live = true, $is_premium = true ) {
4607 $this->_logger->entrance();
4608
4609 $this->dynamic_init( array(
4610 'id' => $id,
4611 'public_key' => $public_key,
4612 'is_live' => $is_live,
4613 'is_premium' => $is_premium,
4614 ) );
4615 }
4616
4617 /**
4618 * Dynamic initiator, originally created to support initiation
4619 * with parent_id for add-ons.
4620 *
4621 * @author Vova Feldman (@svovaf)
4622 * @since 1.0.6
4623 *
4624 * @param array $plugin_info
4625 *
4626 * @throws Freemius_Exception
4627 */
4628 function dynamic_init( array $plugin_info ) {
4629 $this->_logger->entrance();
4630
4631 $this->parse_settings( $plugin_info );
4632
4633 if ( is_admin() && $this->is_theme() && $this->is_premium() && ! $this->has_active_valid_license() ) {
4634 $this->add_ajax_action(
4635 'delete_theme_update_data',
4636 array( &$this, '_delete_theme_update_data_action' )
4637 );
4638 }
4639
4640 if ( ! self::is_ajax() ) {
4641 if ( ! $this->is_addon() || $this->is_only_premium() ) {
4642 add_action(
4643 ( $this->_is_network_active && fs_is_network_admin() ? 'network_' : '' ) . 'admin_menu',
4644 array( &$this, '_prepare_admin_menu' ),
4645 WP_FS__LOWEST_PRIORITY
4646 );
4647 }
4648 }
4649
4650 if ( $this->should_stop_execution() ) {
4651 return;
4652 }
4653
4654 if ( ! $this->is_registered() ) {
4655 if ( $this->is_anonymous() ) {
4656 // If user skipped, no need to test connectivity.
4657 $this->_has_api_connection = true;
4658 $this->_is_on = true;
4659 } else {
4660 if ( ! $this->has_api_connectivity() ) {
4661 if ( $this->_admin_notices->has_sticky( 'failed_connect_api_first' ) ||
4662 $this->_admin_notices->has_sticky( 'failed_connect_api' )
4663 ) {
4664 if ( ! $this->_enable_anonymous || $this->is_premium() ) {
4665 // If anonymous mode is disabled, add firewall admin-notice message.
4666 add_action( 'admin_footer', array( 'Freemius', '_add_firewall_issues_javascript' ) );
4667
4668 $ajax_action_suffix = $this->_slug . ( $this->is_theme() ? ':theme' : '' );
4669 add_action( "wp_ajax_fs_resolve_firewall_issues_{$ajax_action_suffix}", array(
4670 &$this,
4671 '_email_about_firewall_issue'
4672 ) );
4673
4674 add_action( "wp_ajax_fs_retry_connectivity_test_{$ajax_action_suffix}", array(
4675 &$this,
4676 '_retry_connectivity_test'
4677 ) );
4678
4679 /**
4680 * 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.
4681 *
4682 * @author Vova Feldman (@svovaf)
4683 * @since 2.0.0
4684 */
4685 /*$this->add_ajax_action( 'resolve_firewall_issues', array(
4686 &$this,
4687 '_email_about_firewall_issue'
4688 ) );
4689
4690 $this->add_ajax_action( 'retry_connectivity_test', array(
4691 &$this,
4692 '_retry_connectivity_test'
4693 ) );*/
4694 }
4695 }
4696
4697 return;
4698 } else {
4699 $this->_admin_notices->remove_sticky( array(
4700 'failed_connect_api_first',
4701 'failed_connect_api',
4702 ) );
4703
4704 if ( $this->_anonymous_mode ) {
4705 // Simulate anonymous mode.
4706 $this->_is_anonymous = true;
4707 }
4708 }
4709 }
4710 }
4711
4712 /**
4713 * This should be executed even if Freemius is off for the core module,
4714 * otherwise, the add-ons dialogbox won't work properly. This is esepcially
4715 * relevant when the developer decided to turn FS off for existing users.
4716 *
4717 * @author Vova Feldman (@svovaf)
4718 */
4719 if ( $this->is_user_in_admin() &&
4720 'plugin-information' === fs_request_get( 'tab', false ) &&
4721 $this->should_use_freemius_updater_and_dialog() &&
4722 (
4723 ( $this->is_addon() && $this->get_slug() == fs_request_get( 'plugin', false ) ) ||
4724 ( $this->has_addons() && $this->get_id() == fs_request_get( 'parent_plugin_id', false ) )
4725 )
4726 ) {
4727 require_once WP_FS__DIR_INCLUDES . '/fs-plugin-info-dialog.php';
4728
4729 new FS_Plugin_Info_Dialog( $this->is_addon() ? $this->get_parent_instance() : $this );
4730 }
4731
4732 // Check if Freemius is on for the current plugin.
4733 // This MUST be executed after all the plugin variables has been loaded.
4734 if ( ! $this->is_registered() && ! $this->is_on() ) {
4735 return;
4736 }
4737
4738 if ( $this->has_api_connectivity() ) {
4739 if ( self::is_cron() ) {
4740 $this->hook_callback_to_sync_cron();
4741 } else if ( $this->is_user_in_admin() ) {
4742 /**
4743 * Schedule daily data sync cron if:
4744 *
4745 * 1. User opted-in (for tracking).
4746 * 2. If skipped, but later upgraded (opted-in via upgrade).
4747 *
4748 * @author Vova Feldman (@svovaf)
4749 * @since 1.1.7.3
4750 *
4751 */
4752 if ( $this->is_registered() ) {
4753 if ( ! $this->is_sync_cron_on() && $this->is_tracking_allowed() ) {
4754 $this->schedule_sync_cron();
4755 }
4756 }
4757
4758 /**
4759 * Check if requested for manual blocking background sync.
4760 */
4761 if ( fs_request_has( 'background_sync' ) ) {
4762 $this->run_manual_sync();
4763 }
4764 }
4765 }
4766
4767 if ( $this->is_registered() ) {
4768 $this->hook_callback_to_install_sync();
4769 }
4770
4771 if ( $this->is_addon() ) {
4772 if ( $this->is_parent_plugin_installed() ) {
4773 // Link to parent FS.
4774 $this->_parent = self::get_instance_by_id( $this->_plugin->parent_plugin_id );
4775
4776 // Get parent plugin reference.
4777 $this->_parent_plugin = $this->_parent->get_plugin();
4778 }
4779 }
4780
4781 if ( $this->is_user_in_admin() ) {
4782 if ( $this->is_addon() ) {
4783 if ( ! $this->is_parent_plugin_installed() ) {
4784 $parent_name = $this->get_option( $plugin_info, 'parent_name', null );
4785
4786 if ( isset( $plugin_info['parent'] ) ) {
4787 $parent_name = $this->get_option( $plugin_info['parent'], 'name', null );
4788 }
4789
4790 $this->_admin_notices->add(
4791 ( ! empty( $parent_name ) ?
4792 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 ) :
4793 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() )
4794 ),
4795 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
4796 'error'
4797 );
4798
4799 return;
4800 } else {
4801 $is_network_admin = fs_is_network_admin();
4802
4803 if (
4804 $this->_parent->is_registered() &&
4805 ! $this->is_registered() &&
4806 /**
4807 * If not registered for add-on and the following conditions for the add-on are met, activate add-on account.
4808 * * Network active and in network admin - network activate add-on account.
4809 * * Network active and not in network admin - activate add-on account for the current blog.
4810 * * Not network active and not in network admin - activate add-on account for the current blog.
4811 *
4812 * If not registered for add-on, not network active, and in network admin, do not handle the add-on activation.
4813 *
4814 * @author Leo Fajardo (@leorw)
4815 * @since 2.3.0
4816 */
4817 ( $this->is_network_active() || ! $is_network_admin )
4818 ) {
4819 // If parent plugin activated, automatically install add-on for the user.
4820 $this->_activate_addon_account(
4821 $this->_parent,
4822 ( $this->is_network_active() && $is_network_admin ) ?
4823 true :
4824 get_current_blog_id()
4825 );
4826 } else if ( ! $this->_parent->is_registered() && $this->is_registered() ) {
4827 // If add-on activated and parent not, automatically install parent for the user.
4828 $this->activate_parent_account( $this->_parent );
4829 }
4830
4831 // @todo This should be only executed on activation. It should be migrated to register_activation_hook() together with other activation related logic.
4832 if ( $this->is_premium() ) {
4833 // Remove add-on download admin-notice.
4834 $this->_parent->_admin_notices->remove_sticky( array(
4835 'addon_plan_upgraded_' . $this->_slug,
4836 'no_addon_license_' . $this->_slug,
4837 ) );
4838 }
4839
4840 // $this->deactivate_premium_only_addon_without_license();
4841 }
4842 }
4843
4844 add_action( 'admin_init', array( &$this, '_admin_init_action' ) );
4845
4846 // if ( $this->is_registered() ||
4847 // $this->is_anonymous() ||
4848 // $this->is_pending_activation()
4849 // ) {
4850 // $this->_init_admin();
4851 // }
4852 }
4853
4854 /**
4855 * Should be called outside `$this->is_user_in_admin()` scope
4856 * because the updater has some logic that needs to be executed
4857 * during AJAX calls.
4858 *
4859 * Currently we need to hook to the `http_request_host_is_external` filter.
4860 * In the future, there might be additional logic added.
4861 *
4862 * @author Vova Feldman
4863 * @since 1.2.1.6
4864 */
4865 if (
4866 $this->should_use_freemius_updater_and_dialog() &&
4867 (
4868 $this->is_premium() ||
4869 /**
4870 * If not premium but the premium version is installed, also instantiate the updater so that the
4871 * plugin information dialog of the premium version will have the information from the server.
4872 *
4873 * @author Leo Fajardo (@leorw)
4874 * @since 2.2.3
4875 */
4876 ( file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $this->premium_plugin_basename() ) ) )
4877 ) &&
4878 $this->has_release_on_freemius()
4879 ) {
4880 FS_Plugin_Updater::instance( $this );
4881 }
4882
4883 $this->do_action( 'initiated' );
4884
4885 if ( $this->_storage->prev_is_premium !== $this->_plugin->is_premium ) {
4886 if ( isset( $this->_storage->prev_is_premium ) ) {
4887 $this->apply_filters(
4888 'after_code_type_change',
4889 // New code type.
4890 $this->_plugin->is_premium
4891 );
4892 } else {
4893 // Set for code type for the first time.
4894 $this->_storage->prev_is_premium = $this->_plugin->is_premium;
4895 }
4896 }
4897
4898 if ( ! $this->is_addon() ) {
4899 if ( $this->is_registered() ) {
4900 // Fix for upgrade from versions < 1.0.9.
4901 if ( ! isset( $this->_storage->activation_timestamp ) ) {
4902 $this->_storage->activation_timestamp = WP_FS__SCRIPT_START_TIME;
4903 }
4904
4905 $this->do_action( 'after_init_plugin_registered' );
4906 } else if ( $this->is_anonymous() ) {
4907 $this->do_action( 'after_init_plugin_anonymous' );
4908 } else if ( $this->is_pending_activation() ) {
4909 $this->do_action( 'after_init_plugin_pending_activations' );
4910 }
4911 } else {
4912 if ( $this->is_registered() ) {
4913 $this->do_action( 'after_init_addon_registered' );
4914 } else if ( $this->is_anonymous() ) {
4915 $this->do_action( 'after_init_addon_anonymous' );
4916 } else if ( $this->is_pending_activation() ) {
4917 $this->do_action( 'after_init_addon_pending_activations' );
4918 }
4919 }
4920 }
4921
4922 /**
4923 * @author Leo Fajardo (@leorw)
4924 * @since 2.2.3
4925 *
4926 * @return bool
4927 */
4928 private function should_use_freemius_updater_and_dialog() {
4929 return (
4930 /**
4931 * Allow updater and dialog when the `fs_allow_updater_and_dialog` URL query param exists and has `true`
4932 * value, or when the current page is not the "Add Plugins" page (/plugin-install.php) and the `action`
4933 * URL query param doesn't exist or its value is not `install-plugin` so that there will be no conflicts
4934 * with the .org plugins' functionalities (e.g. installation from the "Add Plugins" page and viewing
4935 * plugin details from .org).
4936 */
4937 ( true === fs_request_get_bool( 'fs_allow_updater_and_dialog' ) ) ||
4938 (
4939 ! self::is_plugin_install_page() &&
4940 // Disallow updater and dialog when installing a plugin, otherwise .org "add-on" plugins will be affected.
4941 ( 'install-plugin' !== fs_request_get( 'action' ) )
4942 )
4943 );
4944 }
4945
4946 /**
4947 * @author Leo Fajardo (@leorw)
4948 *
4949 * @since 1.2.1.5
4950 */
4951 function _stop_tracking_callback() {
4952 $this->_logger->entrance();
4953
4954 $this->check_ajax_referer( 'stop_tracking' );
4955
4956 $result = $this->stop_tracking( fs_is_network_admin() );
4957
4958 if ( true === $result ) {
4959 self::shoot_ajax_success();
4960 }
4961
4962 $this->_logger->api_error( $result );
4963
4964 self::shoot_ajax_failure(
4965 sprintf( $this->get_text_inline( 'Unexpected API error. Please contact the %s\'s author with the following error.', 'unexpected-api-error' ), $this->_module_type ) .
4966 ( $this->is_api_error( $result ) && isset( $result->error ) ?
4967 $result->error->message :
4968 var_export( $result, true ) )
4969 );
4970 }
4971
4972 /**
4973 * @author Leo Fajardo (@leorw)
4974 * @since 1.2.1.5
4975 */
4976 function _allow_tracking_callback() {
4977 $this->_logger->entrance();
4978
4979 $this->check_ajax_referer( 'allow_tracking' );
4980
4981 $result = $this->allow_tracking( fs_is_network_admin() );
4982
4983 if ( true === $result ) {
4984 self::shoot_ajax_success();
4985 }
4986
4987 $this->_logger->api_error( $result );
4988
4989 self::shoot_ajax_failure(
4990 sprintf( $this->get_text_inline( 'Unexpected API error. Please contact the %s\'s author with the following error.', 'unexpected-api-error' ), $this->_module_type ) .
4991 ( $this->is_api_error( $result ) && isset( $result->error ) ?
4992 $result->error->message :
4993 var_export( $result, true ) )
4994 );
4995 }
4996
4997 /**
4998 * Opt-out from usage tracking.
4999 *
5000 * Note: This will not delete the account information but will stop all tracking.
5001 *
5002 * Returns:
5003 * 1. FALSE - If the user never opted-in.
5004 * 2. TRUE - If successfully opted-out.
5005 * 3. object - API result on failure.
5006 *
5007 * @author Leo Fajardo (@leorw)
5008 * @since 1.2.1.5
5009 *
5010 * @return bool|object
5011 */
5012 function stop_site_tracking() {
5013 $this->_logger->entrance();
5014
5015 if ( ! $this->is_registered() ) {
5016 // User never opted-in.
5017 return false;
5018 }
5019
5020 if ( $this->is_tracking_prohibited() ) {
5021 // Already disconnected.
5022 return true;
5023 }
5024
5025 // Send update to FS.
5026 $result = $this->get_api_site_scope()->call( '/?fields=is_disconnected', 'put', array(
5027 'is_disconnected' => true
5028 ) );
5029
5030 if ( ! $this->is_api_result_entity( $result ) ||
5031 ! isset( $result->is_disconnected ) ||
5032 ! $result->is_disconnected
5033 ) {
5034 $this->_logger->api_error( $result );
5035
5036 return $result;
5037 }
5038
5039 $this->_site->is_disconnected = $result->is_disconnected;
5040 $this->_store_site();
5041
5042 $this->clear_sync_cron();
5043
5044 // Successfully disconnected.
5045 return true;
5046 }
5047
5048 /**
5049 * Opt-out network from usage tracking.
5050 *
5051 * Note: This will not delete the account information but will stop all tracking.
5052 *
5053 * Returns:
5054 * 1. FALSE - If the user never opted-in.
5055 * 2. TRUE - If successfully opted-out.
5056 * 3. object - API result on failure.
5057 *
5058 * @author Leo Fajardo (@leorw)
5059 * @since 1.2.1.5
5060 *
5061 * @return bool|object
5062 */
5063 function stop_network_tracking() {
5064 $this->_logger->entrance();
5065
5066 if ( ! $this->is_registered() ) {
5067 // User never opted-in.
5068 return false;
5069 }
5070
5071 $install_id_2_blog_id = array();
5072 $installs_map = $this->get_blog_install_map();
5073
5074 $opt_out_all = true;
5075
5076 $params = array();
5077 foreach ( $installs_map as $blog_id => $install ) {
5078 if ( $install->is_tracking_prohibited() ) {
5079 // Already opted-out.
5080 continue;
5081 }
5082
5083 if ( $this->is_site_delegated_connection( $blog_id ) ) {
5084 // Opt-out only from non-delegated installs.
5085 $opt_out_all = false;
5086 continue;
5087 }
5088
5089 $params[] = array( 'id' => $install->id );
5090
5091 $install_id_2_blog_id[ $install->id ] = $blog_id;
5092 }
5093
5094 if ( empty( $install_id_2_blog_id ) ) {
5095 return true;
5096 }
5097
5098 $params[] = array( 'is_disconnected' => true );
5099
5100 // Send update to FS.
5101 $result = $this->get_current_or_network_user_api_scope()->call( "/plugins/{$this->_module_id}/installs.json", 'put', $params );
5102
5103 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
5104 $this->_logger->api_error( $result );
5105
5106 return $result;
5107 }
5108
5109 foreach ( $result->installs as $r_install ) {
5110 $blog_id = $install_id_2_blog_id[ $r_install->id ];
5111 $install = $installs_map[ $blog_id ];
5112 $install->is_disconnected = $r_install->is_disconnected;
5113 $this->_store_site( true, $blog_id, $install );
5114 }
5115
5116 $this->clear_sync_cron( $opt_out_all );
5117
5118 // Successfully disconnected.
5119 return true;
5120 }
5121
5122 /**
5123 * Opt-out from usage tracking.
5124 *
5125 * Note: This will not delete the account information but will stop all tracking.
5126 *
5127 * Returns:
5128 * 1. FALSE - If the user never opted-in.
5129 * 2. TRUE - If successfully opted-out.
5130 * 3. object - API result on failure.
5131 *
5132 * @author Leo Fajardo (@leorw)
5133 * @since 1.2.1.5
5134 *
5135 * @param bool $is_network_action
5136 *
5137 * @return bool|object
5138 */
5139 function stop_tracking( $is_network_action = false ) {
5140 $this->_logger->entrance();
5141
5142 return $is_network_action ?
5143 $this->stop_network_tracking() :
5144 $this->stop_site_tracking();
5145 }
5146
5147 /**
5148 * Opt-in back into usage tracking.
5149 *
5150 * Note: This will only work if the user opted-in previously.
5151 *
5152 * Returns:
5153 * 1. FALSE - If the user never opted-in.
5154 * 2. TRUE - If successfully opted-in back to usage tracking.
5155 * 3. object - API result on failure.
5156 *
5157 * @author Leo Fajardo (@leorw)
5158 * @since 1.2.1.5
5159 *
5160 * @return bool|object
5161 */
5162 function allow_site_tracking() {
5163 $this->_logger->entrance();
5164
5165 if ( ! $this->is_registered() ) {
5166 // User never opted-in.
5167 return false;
5168 }
5169
5170 if ( $this->is_tracking_allowed() ) {
5171 // Tracking already allowed.
5172 return true;
5173 }
5174
5175 $result = $this->get_api_site_scope()->call( '/?is_disconnected', 'put', array(
5176 'is_disconnected' => false
5177 ) );
5178
5179 if ( ! $this->is_api_result_entity( $result ) ||
5180 ! isset( $result->is_disconnected ) ||
5181 $result->is_disconnected
5182 ) {
5183 $this->_logger->api_error( $result );
5184
5185 return $result;
5186 }
5187
5188 $this->_site->is_disconnected = $result->is_disconnected;
5189 $this->_store_site();
5190
5191 $this->schedule_sync_cron();
5192
5193 // Successfully reconnected.
5194 return true;
5195 }
5196
5197 /**
5198 * Opt-in network back into usage tracking.
5199 *
5200 * Note: This will only work if the user opted-in previously.
5201 *
5202 * Returns:
5203 * 1. FALSE - If the user never opted-in.
5204 * 2. TRUE - If successfully opted-in back to usage tracking.
5205 * 3. object - API result on failure.
5206 *
5207 * @author Leo Fajardo (@leorw)
5208 * @since 1.2.1.5
5209 *
5210 * @return bool|object
5211 */
5212 function allow_network_tracking() {
5213 $this->_logger->entrance();
5214
5215 if ( ! $this->is_registered() ) {
5216 // User never opted-in.
5217 return false;
5218 }
5219
5220 $install_id_2_blog_id = array();
5221 $installs_map = $this->get_blog_install_map();
5222
5223 $params = array();
5224 foreach ( $installs_map as $blog_id => $install ) {
5225 if ( $install->is_tracking_allowed() ) {
5226 continue;
5227 }
5228
5229 $params[] = array( 'id' => $install->id );
5230
5231 $install_id_2_blog_id[ $install->id ] = $blog_id;
5232 }
5233
5234 if ( empty( $install_id_2_blog_id ) ) {
5235 return true;
5236 }
5237
5238 $params[] = array( 'is_disconnected' => false );
5239
5240 // Send update to FS.
5241 $result = $this->get_current_or_network_user_api_scope()->call( "/plugins/{$this->_module_id}/installs.json", 'put', $params );
5242
5243
5244 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
5245 $this->_logger->api_error( $result );
5246
5247 return $result;
5248 }
5249
5250 foreach ( $result->installs as $r_install ) {
5251 $blog_id = $install_id_2_blog_id[ $r_install->id ];
5252 $install = $installs_map[ $blog_id ];
5253 $install->is_disconnected = $r_install->is_disconnected;
5254 $this->_store_site( true, $blog_id, $install );
5255 }
5256
5257 $this->schedule_sync_cron();
5258
5259 // Successfully reconnected.
5260 return true;
5261 }
5262
5263 /**
5264 * Opt-in back into usage tracking.
5265 *
5266 * Note: This will only work if the user opted-in previously.
5267 *
5268 * Returns:
5269 * 1. FALSE - If the user never opted-in.
5270 * 2. TRUE - If successfully opted-in back to usage tracking.
5271 * 3. object - API result on failure.
5272 *
5273 * @author Leo Fajardo (@leorw)
5274 * @since 1.2.1.5
5275 *
5276 * @param bool $is_network_action
5277 *
5278 * @return bool|object
5279 */
5280 function allow_tracking( $is_network_action = false ) {
5281 $this->_logger->entrance();
5282
5283 return $is_network_action ?
5284 $this->allow_network_tracking() :
5285 $this->allow_site_tracking();
5286 }
5287
5288 /**
5289 * If user opted-in and later disabled usage-tracking,
5290 * re-allow tracking for licensing and updates.
5291 *
5292 * @author Leo Fajardo (@leorw)
5293 * @since 1.2.1.5
5294 *
5295 * @param bool $is_context_single_site
5296 */
5297 private function reconnect_locally( $is_context_single_site = false ) {
5298 $this->_logger->entrance();
5299
5300 if ( ! $this->is_registered() ) {
5301 return;
5302 }
5303
5304 if ( ! fs_is_network_admin() || $is_context_single_site ) {
5305 if ( $this->is_tracking_prohibited() ) {
5306 $this->_site->is_disconnected = false;
5307 $this->_store_site();
5308 }
5309 } else {
5310 $installs_map = $this->get_blog_install_map();
5311 foreach ( $installs_map as $blog_id => $install ) {
5312 /**
5313 * @var FS_Site $install
5314 */
5315 if ( $install->is_tracking_prohibited() ) {
5316 $install->is_disconnected = false;
5317 $this->_store_site( true, $blog_id, $install );
5318 }
5319 }
5320 }
5321 }
5322
5323 /**
5324 * Parse plugin's settings (as defined by the plugin dev).
5325 *
5326 * @author Vova Feldman (@svovaf)
5327 * @since 1.1.7.3
5328 *
5329 * @param array $plugin_info
5330 *
5331 * @throws \Freemius_Exception
5332 */
5333 private function parse_settings( &$plugin_info ) {
5334 $this->_logger->entrance();
5335
5336 $id = $this->get_numeric_option( $plugin_info, 'id', false );
5337 $public_key = $this->get_option( $plugin_info, 'public_key', false );
5338 $secret_key = $this->get_option( $plugin_info, 'secret_key', null );
5339 $parent_id = $this->get_numeric_option( $plugin_info, 'parent_id', null );
5340 $parent_name = $this->get_option( $plugin_info, 'parent_name', null );
5341
5342 /**
5343 * @author Vova Feldman (@svovaf)
5344 * @since 1.1.9 Try to pull secret key from external config.
5345 */
5346 if ( is_null( $secret_key ) && defined( "WP_FS__{$this->_slug}_SECRET_KEY" ) ) {
5347 $secret_key = constant( "WP_FS__{$this->_slug}_SECRET_KEY" );
5348 }
5349
5350 if ( isset( $plugin_info['parent'] ) ) {
5351 $parent_id = $this->get_numeric_option( $plugin_info['parent'], 'id', null );
5352 // $parent_slug = $this->get_option( $plugin_info['parent'], 'slug', null );
5353 // $parent_public_key = $this->get_option( $plugin_info['parent'], 'public_key', null );
5354 // $parent_name = $this->get_option( $plugin_info['parent'], 'name', null );
5355 }
5356
5357 if ( false === $id ) {
5358 throw new Freemius_Exception( array(
5359 'error' => array(
5360 'type' => 'ParameterNotSet',
5361 'message' => 'Plugin id parameter is not set.',
5362 'code' => 'plugin_id_not_set',
5363 'http' => 500,
5364 )
5365 ) );
5366 }
5367 if ( false === $public_key ) {
5368 throw new Freemius_Exception( array(
5369 'error' => array(
5370 'type' => 'ParameterNotSet',
5371 'message' => 'Plugin public_key parameter is not set.',
5372 'code' => 'plugin_public_key_not_set',
5373 'http' => 500,
5374 )
5375 ) );
5376 }
5377
5378 $plugin = ( $this->_plugin instanceof FS_Plugin ) ?
5379 $this->_plugin :
5380 new FS_Plugin();
5381
5382 $premium_suffix = $this->get_option( $plugin_info, 'premium_suffix', '(Premium)' );
5383
5384 $plugin->update( array(
5385 'id' => $id,
5386 'type' => $this->get_option( $plugin_info, 'type', $this->_module_type ),
5387 'public_key' => $public_key,
5388 'slug' => $this->_slug,
5389 'premium_slug' => $this->get_option( $plugin_info, 'premium_slug', "{$this->_slug}-premium" ),
5390 'parent_plugin_id' => $parent_id,
5391 'version' => $this->get_plugin_version(),
5392 'title' => $this->get_plugin_name( $premium_suffix ),
5393 'file' => $this->_plugin_basename,
5394 'is_premium' => $this->get_bool_option( $plugin_info, 'is_premium', true ),
5395 'premium_suffix' => $premium_suffix,
5396 'is_live' => $this->get_bool_option( $plugin_info, 'is_live', true ),
5397 'affiliate_moderation' => $this->get_option( $plugin_info, 'has_affiliation' ),
5398 'bundle_id' => $this->get_option( $plugin_info, 'bundle_id', null ),
5399 ) );
5400
5401 if ( $plugin->is_updated() ) {
5402 // Update plugin details.
5403 $this->_plugin = FS_Plugin_Manager::instance( $this->_module_id )->store( $plugin );
5404 }
5405 // Set the secret key after storing the plugin, we don't want to store the key in the storage.
5406 $this->_plugin->secret_key = $secret_key;
5407
5408 /**
5409 * 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).
5410 *
5411 * @author Vova Feldman
5412 * @since 2.4.5
5413 */
5414 if ( $this->is_network_active() && fs_is_network_admin() ) {
5415 if ( isset( $plugin_info['menu_network'] ) &&
5416 is_array( $plugin_info['menu_network'] ) &&
5417 ! empty( $plugin_info['menu_network'] )
5418 ) {
5419 $plugin_info['menu'] = $plugin_info['menu_network'];
5420 }
5421 }
5422
5423 if ( ! isset( $plugin_info['menu'] ) ) {
5424 $plugin_info['menu'] = array();
5425
5426 if ( ! empty( $this->_storage->sdk_last_version ) &&
5427 version_compare( $this->_storage->sdk_last_version, '1.1.2', '<=' )
5428 ) {
5429 // Backward compatibility to 1.1.2
5430 $plugin_info['menu']['slug'] = isset( $plugin_info['menu_slug'] ) ?
5431 $plugin_info['menu_slug'] :
5432 $this->_slug;
5433 }
5434 }
5435
5436 $this->_menu = FS_Admin_Menu_Manager::instance(
5437 $this->_module_id,
5438 $this->_module_type,
5439 $this->get_unique_affix()
5440 );
5441
5442 $this->_menu->init( $plugin_info['menu'], $this->is_addon() );
5443
5444 $this->_has_addons = $this->get_bool_option( $plugin_info, 'has_addons', false );
5445 $this->_has_paid_plans = $this->get_bool_option( $plugin_info, 'has_paid_plans', true );
5446 $this->_has_premium_version = $this->get_bool_option( $plugin_info, 'has_premium_version', $this->_has_paid_plans );
5447 $this->_ignore_pending_mode = $this->get_bool_option( $plugin_info, 'ignore_pending_mode', false );
5448 $this->_is_org_compliant = $this->get_bool_option( $plugin_info, 'is_org_compliant', true );
5449 $this->_is_premium_only = $this->get_bool_option( $plugin_info, 'is_premium_only', false );
5450 if ( $this->_is_premium_only ) {
5451 // If premium only plugin, disable anonymous mode.
5452 $this->_enable_anonymous = false;
5453 $this->_anonymous_mode = false;
5454 } else {
5455 $this->_enable_anonymous = $this->get_bool_option( $plugin_info, 'enable_anonymous', true );
5456 $this->_anonymous_mode = $this->get_bool_option( $plugin_info, 'anonymous_mode', false );
5457 }
5458 $this->_permissions = $this->get_option( $plugin_info, 'permissions', array() );
5459
5460 if ( ! empty( $plugin_info['trial'] ) ) {
5461 $this->_trial_days = $this->get_numeric_option(
5462 $plugin_info['trial'],
5463 'days',
5464 // Default to 0 - trial without days specification.
5465 0
5466 );
5467
5468 $this->_is_trial_require_payment = $this->get_bool_option( $plugin_info['trial'], 'is_require_payment', false );
5469 }
5470 }
5471
5472 /**
5473 * @param string[] $options
5474 * @param string $key
5475 * @param mixed $default
5476 *
5477 * @return bool
5478 */
5479 private function get_option( &$options, $key, $default = false ) {
5480 return ! empty( $options[ $key ] ) ? $options[ $key ] : $default;
5481 }
5482
5483 private function get_bool_option( &$options, $key, $default = false ) {
5484 return isset( $options[ $key ] ) && is_bool( $options[ $key ] ) ? $options[ $key ] : $default;
5485 }
5486
5487 private function get_numeric_option( &$options, $key, $default = false ) {
5488 return isset( $options[ $key ] ) && is_numeric( $options[ $key ] ) ? $options[ $key ] : $default;
5489 }
5490
5491 /**
5492 * Gate keeper.
5493 *
5494 * @author Vova Feldman (@svovaf)
5495 * @since 1.1.7.3
5496 *
5497 * @return bool
5498 */
5499 private function should_stop_execution() {
5500 if ( empty( $this->_storage->was_plugin_loaded ) ) {
5501 /**
5502 * Don't execute Freemius until plugin was fully loaded at least once,
5503 * to give the opportunity for the activation hook to run before pinging
5504 * the API for connectivity test. This logic is relevant for the
5505 * identification of new plugin install vs. plugin update.
5506 *
5507 * @author Vova Feldman (@svovaf)
5508 * @since 1.1.9
5509 */
5510 return true;
5511 }
5512
5513 if ( $this->is_activation_mode() ) {
5514 if ( ! is_admin() ) {
5515 /**
5516 * If in activation mode, don't execute Freemius outside of the
5517 * admin dashboard.
5518 *
5519 * @author Vova Feldman (@svovaf)
5520 * @since 1.1.7.3
5521 */
5522 return true;
5523 }
5524
5525 if ( ! WP_FS__IS_HTTP_REQUEST ) {
5526 /**
5527 * If in activation and executed without HTTP context (e.g. CLI, Cronjob),
5528 * then don't start Freemius.
5529 *
5530 * @author Vova Feldman (@svovaf)
5531 * @since 1.1.6.3
5532 *
5533 * @link https://wordpress.org/support/topic/errors-in-the-freemius-class-when-running-in-wordpress-in-cli
5534 */
5535 return true;
5536 }
5537
5538 if ( self::is_cron() ) {
5539 /**
5540 * If in activation mode, don't execute Freemius during wp crons
5541 * (wp crons have HTTP context - called as HTTP request).
5542 *
5543 * @author Vova Feldman (@svovaf)
5544 * @since 1.1.7.3
5545 */
5546 return true;
5547 }
5548
5549 if ( self::is_ajax() &&
5550 ! $this->_admin_notices->has_sticky( 'failed_connect_api_first' ) &&
5551 ! $this->_admin_notices->has_sticky( 'failed_connect_api' )
5552 ) {
5553 /**
5554 * During activation, if running in AJAX mode, unless there's a sticky
5555 * connectivity issue notice, don't run Freemius.
5556 *
5557 * @author Vova Feldman (@svovaf)
5558 * @since 1.1.7.3
5559 */
5560 return true;
5561 }
5562 }
5563
5564 return false;
5565 }
5566
5567 /**
5568 * Triggered after code type has changed.
5569 *
5570 * @author Vova Feldman (@svovaf)
5571 * @since 1.1.9.1
5572 */
5573 function _after_code_type_change() {
5574 $this->_logger->entrance();
5575
5576 if ( $this->is_theme() ) {
5577 // Expire the cache of the previous tabs since the theme may
5578 // have setting updates after code type has changed.
5579 $this->_cache->expire( 'tabs' );
5580 $this->_cache->expire( 'tabs_stylesheets' );
5581 }
5582
5583 if ( $this->is_registered() ) {
5584 if ( ! $this->is_addon() ) {
5585 add_action(
5586 is_admin() ? 'admin_init' : 'init',
5587 array( &$this, '_plugin_code_type_changed' )
5588 );
5589 }
5590
5591 if ( $this->is_premium() ) {
5592 // Purge cached payments after switching to the premium version.
5593 // @todo This logic doesn't handle purging the cache for serviceware module upgrade.
5594 $this->get_api_user_scope()->purge_cache( "/plugins/{$this->_module_id}/payments.json?include_addons=true" );
5595 }
5596 }
5597 }
5598
5599 /**
5600 * Handles plugin's code type change (free <--> premium).
5601 *
5602 * @author Vova Feldman (@svovaf)
5603 * @since 1.0.9
5604 */
5605 function _plugin_code_type_changed() {
5606 $this->_logger->entrance();
5607
5608 if ( $this->is_premium() ) {
5609 $this->reconnect_locally();
5610
5611 // Activated premium code.
5612 $this->do_action( 'after_premium_version_activation' );
5613
5614 // Remove all sticky messages related to download of the premium version.
5615 $this->_admin_notices->remove_sticky( array(
5616 'trial_started',
5617 'plan_upgraded',
5618 'plan_changed',
5619 'license_activated',
5620 ) );
5621
5622 $notice = '';
5623 if ( ! $this->is_only_premium() ) {
5624 $notice = sprintf( $this->get_text_inline( 'Premium %s version was successfully activated.', 'premium-activated-message' ), $this->_module_type );
5625 }
5626
5627 $license_notice = $this->get_license_network_activation_notice();
5628 if ( ! empty( $license_notice ) ) {
5629 $notice .= ' ' . $license_notice;
5630 }
5631
5632 if ( ! empty( $notice ) ) {
5633 $this->_admin_notices->add_sticky(
5634 trim( $notice ),
5635 'premium_activated',
5636 $this->get_text_x_inline( 'W00t',
5637 'Used to express elation, enthusiasm, or triumph (especially in electronic communication).', 'woot' ) . '!'
5638 );
5639 }
5640 } else {
5641 // Remove sticky message related to premium code activation.
5642 $this->_admin_notices->remove_sticky( 'premium_activated' );
5643
5644 // Activated free code (after had the premium before).
5645 $this->do_action( 'after_free_version_reactivation' );
5646
5647 if ( $this->is_paying() && ! $this->is_premium() ) {
5648 $this->_admin_notices->add_sticky(
5649 sprintf(
5650 /* translators: %s: License type (e.g. you have a professional license) */
5651 $this->get_text_inline( 'You have a %s license.', 'you-have-x-license' ),
5652 $this->get_plan_title()
5653 ) . $this->get_complete_upgrade_instructions(),
5654 'plan_upgraded',
5655 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
5656 );
5657 }
5658 }
5659
5660 // Schedule code type changes event.
5661 $this->schedule_install_sync();
5662
5663 /**
5664 * Unregister the uninstall hook for the other version of the plugin (with different code type) to avoid
5665 * triggering a fatal error when uninstalling that plugin. For example, after deactivating the "free" version
5666 * of a specific plugin, its uninstall hook should be unregistered after the "premium" version has been
5667 * activated. If we don't do that, a fatal error will occur when we try to uninstall the "free" version since
5668 * the main file of the "free" version will be loaded first before calling the hooked callback. Since the
5669 * free and premium versions are almost identical (same class or have same functions), a fatal error like
5670 * "Cannot redeclare class MyClass" or "Cannot redeclare my_function()" will occur.
5671 */
5672 $this->unregister_uninstall_hook();
5673
5674 $this->clear_module_main_file_cache();
5675
5676 // Update is_premium of latest version.
5677 $this->_storage->prev_is_premium = $this->_plugin->is_premium;
5678 }
5679
5680 #endregion
5681
5682 #----------------------------------------------------------------------------------
5683 #region Add-ons
5684 #----------------------------------------------------------------------------------
5685
5686 /**
5687 * Check if add-on installed and activated on site.
5688 *
5689 * @author Vova Feldman (@svovaf)
5690 * @since 1.0.6
5691 *
5692 * @param string|number $id_or_slug
5693 * @param bool|null $is_premium Since 1.2.1.7 can check for specified add-on version.
5694 *
5695 * @return bool
5696 */
5697 function is_addon_activated( $id_or_slug, $is_premium = null ) {
5698 $this->_logger->entrance();
5699
5700 $addon_id = self::get_module_id( $id_or_slug );
5701 $is_activated = self::has_instance( $addon_id );
5702
5703 if ( ! $is_activated ) {
5704 return false;
5705 }
5706
5707 if ( is_bool( $is_premium ) ) {
5708 // Check if the specified code version is activate.
5709 $addon = $this->get_addon_instance( $addon_id );
5710 $is_activated = ( $is_premium === $addon->is_premium() );
5711 }
5712
5713 return $is_activated;
5714 }
5715
5716 /**
5717 * Check if add-on was connected to install
5718 *
5719 * @author Vova Feldman (@svovaf)
5720 * @since 1.1.7
5721 *
5722 * @param string|number $id_or_slug
5723 *
5724 * @return bool
5725 */
5726 function is_addon_connected( $id_or_slug ) {
5727 $this->_logger->entrance();
5728
5729 $sites = self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN );
5730
5731 $addon_id = self::get_module_id( $id_or_slug );
5732 $addon = $this->get_addon( $addon_id );
5733 $slug = $addon->slug;
5734 if ( ! isset( $sites[ $slug ] ) ) {
5735 return false;
5736 }
5737
5738 $site = $sites[ $slug ];
5739
5740 $plugin = FS_Plugin_Manager::instance( $addon_id )->get();
5741
5742 if ( $plugin->parent_plugin_id != $this->_plugin->id ) {
5743 // The given slug do NOT belong to any of the plugin's add-ons.
5744 return false;
5745 }
5746
5747 return ( is_object( $site ) &&
5748 is_numeric( $site->id ) &&
5749 is_numeric( $site->user_id ) &&
5750 FS_Plugin_Plan::is_valid_id( $site->plan_id )
5751 );
5752 }
5753
5754 /**
5755 * Determines if add-on installed.
5756 *
5757 * NOTE: This is a heuristic and only works if the folder/file named as the slug.
5758 *
5759 * @author Vova Feldman (@svovaf)
5760 * @since 1.0.6
5761 *
5762 * @param string|number $id_or_slug
5763 *
5764 * @return bool
5765 */
5766 function is_addon_installed( $id_or_slug ) {
5767 $this->_logger->entrance();
5768
5769 $addon_id = self::get_module_id( $id_or_slug );
5770
5771 return file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $this->get_addon_basename( $addon_id ) ) );
5772 }
5773
5774 /**
5775 * Get add-on basename.
5776 *
5777 * @author Vova Feldman (@svovaf)
5778 * @since 1.0.6
5779 *
5780 * @param string|number $id_or_slug
5781 *
5782 * @return string
5783 */
5784 function get_addon_basename( $id_or_slug ) {
5785 $addon_id = self::get_module_id( $id_or_slug );
5786
5787 if ( $this->is_addon_activated( $addon_id ) ) {
5788 return self::instance( $addon_id )->get_plugin_basename();
5789 }
5790
5791 $addon = $this->get_addon( $addon_id );
5792 $premium_basename = "{$addon->premium_slug}/{$addon->slug}.php";
5793
5794 if ( file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $premium_basename ) ) ) {
5795 return $premium_basename;
5796 }
5797
5798 $all_plugins = $this->get_all_plugins();
5799
5800 foreach ( $all_plugins as $basename => $data ) {
5801 if ( $addon->slug === $data['slug'] ||
5802 $addon->premium_slug === $data['slug']
5803 ) {
5804 return $basename;
5805 }
5806 }
5807
5808 $free_basename = "{$addon->slug}/{$addon->slug}.php";
5809
5810 return $free_basename;
5811 }
5812
5813 /**
5814 * Get installed add-ons instances.
5815 *
5816 * @author Vova Feldman (@svovaf)
5817 * @since 1.0.6
5818 *
5819 * @return Freemius[]
5820 */
5821 function get_installed_addons() {
5822 $installed_addons = array();
5823 foreach ( self::$_instances as $instance ) {
5824 if ( $instance->is_addon() && is_object( $instance->_parent_plugin ) ) {
5825 if ( $this->_plugin->id == $instance->_parent_plugin->id ) {
5826 $installed_addons[] = $instance;
5827 }
5828 }
5829 }
5830
5831 return $installed_addons;
5832 }
5833
5834 /**
5835 * Check if any add-ons of the plugin are installed.
5836 *
5837 * @author Leo Fajardo (@leorw)
5838 * @since 1.1.1
5839 *
5840 * @return bool
5841 */
5842 function has_installed_addons() {
5843 if ( ! $this->has_addons() ) {
5844 return false;
5845 }
5846
5847 foreach ( self::$_instances as $instance ) {
5848 if ( $instance->is_addon() && is_object( $instance->_parent_plugin ) ) {
5849 if ( $this->_plugin->id == $instance->_parent_plugin->id ) {
5850 return true;
5851 }
5852 }
5853 }
5854
5855 return false;
5856 }
5857
5858 /**
5859 * Tell Freemius that the current plugin is an add-on.
5860 *
5861 * @author Vova Feldman (@svovaf)
5862 * @since 1.0.6
5863 *
5864 * @param number $parent_plugin_id The parent plugin ID
5865 */
5866 function init_addon( $parent_plugin_id ) {
5867 $this->_plugin->parent_plugin_id = $parent_plugin_id;
5868 }
5869
5870 /**
5871 * @author Vova Feldman (@svovaf)
5872 * @since 1.0.6
5873 *
5874 * @return bool
5875 */
5876 function is_addon() {
5877 return isset( $this->_plugin->parent_plugin_id ) && is_numeric( $this->_plugin->parent_plugin_id );
5878 }
5879
5880 /**
5881 * Deactivate add-on if it's premium only and the user does't have a valid license.
5882 *
5883 * @param bool $is_after_trial_cancel
5884 *
5885 * @return bool If add-on was deactivated.
5886 */
5887 private function deactivate_premium_only_addon_without_license( $is_after_trial_cancel = false ) {
5888 if ( ! $this->has_free_plan() &&
5889 ! $this->has_features_enabled_license() &&
5890 ! $this->_has_premium_license()
5891 ) {
5892 if ( $this->is_registered() ) {
5893 // IF wrapper is turned off because activation_timestamp is currently only stored for plugins (not addons).
5894 // if (empty($this->_storage->activation_timestamp) ||
5895 // (WP_FS__SCRIPT_START_TIME - $this->_storage->activation_timestamp) > 30
5896 // ) {
5897 /**
5898 * @todo When it's first fail, there's no reason to try and re-sync because the licenses were just synced after initial activation.
5899 *
5900 * Retry syncing the user add-on licenses.
5901 */
5902 // Sync licenses.
5903 $this->_sync_licenses();
5904 // }
5905
5906 // Try to activate premium license.
5907 $this->_activate_license( true );
5908 }
5909
5910 if ( ! $this->has_free_plan() &&
5911 ! $this->has_features_enabled_license() &&
5912 ! $this->_has_premium_license()
5913 ) {
5914 // @todo Check if deactivate plugins also call the deactivation hook.
5915
5916 $this->_parent->_admin_notices->add_sticky(
5917 sprintf(
5918 ( $is_after_trial_cancel ?
5919 $this->_parent->get_text_inline(
5920 '%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.',
5921 'addon-trial-cancelled-message'
5922 ) :
5923 $this->_parent->get_text_inline(
5924 '%s is a premium only add-on. You have to purchase a license first before activating the plugin.',
5925 'addon-no-license-message'
5926 )
5927 ),
5928 '<b>' . $this->_plugin->title . '</b>'
5929 ) . ' ' . sprintf(
5930 '<a href="%s" aria-label="%s" class="button button-primary" style="margin-left: 10px; vertical-align: middle;">%s &nbsp;&#10140;</a>',
5931 $this->_parent->addon_url( $this->_slug ),
5932 esc_attr( sprintf( $this->_parent->get_text_inline( 'More information about %s', 'more-information-about-x' ), $this->_plugin->title ) ),
5933 $this->_parent->get_text_inline( 'Purchase License', 'purchase-license' )
5934 ),
5935 'no_addon_license_' . $this->_slug,
5936 ( $is_after_trial_cancel ? '' : $this->_parent->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...' ),
5937 ( $is_after_trial_cancel ? 'success' : 'error' )
5938 );
5939
5940 deactivate_plugins( array( $this->_plugin_basename ), true );
5941
5942 return true;
5943 }
5944 }
5945
5946 return false;
5947 }
5948
5949 #endregion
5950
5951 #----------------------------------------------------------------------------------
5952 #region Sandbox
5953 #----------------------------------------------------------------------------------
5954
5955 /**
5956 * Set Freemius into sandbox mode for debugging.
5957 *
5958 * @author Vova Feldman (@svovaf)
5959 * @since 1.0.4
5960 *
5961 * @param string $secret_key
5962 */
5963 function init_sandbox( $secret_key ) {
5964 $this->_plugin->secret_key = $secret_key;
5965
5966 // Update plugin details.
5967 FS_Plugin_Manager::instance( $this->_module_id )->update( $this->_plugin, true );
5968 }
5969
5970 /**
5971 * Check if running payments in sandbox mode.
5972 *
5973 * @author Vova Feldman (@svovaf)
5974 * @since 1.0.4
5975 *
5976 * @return bool
5977 */
5978 function is_payments_sandbox() {
5979 return ( ! $this->is_live() ) || isset( $this->_plugin->secret_key );
5980 }
5981
5982 #endregion
5983
5984 /**
5985 * Check if running test vs. live plugin.
5986 *
5987 * @author Vova Feldman (@svovaf)
5988 * @since 1.0.5
5989 *
5990 * @return bool
5991 */
5992 function is_live() {
5993 return $this->_plugin->is_live;
5994 }
5995
5996 /**
5997 * Check if super-admin skipped connection for all sites in the network.
5998 *
5999 * @author Vova Feldman (@svovaf)
6000 * @since 2.0.0
6001 */
6002 function is_network_anonymous() {
6003 if ( ! $this->_is_network_active ) {
6004 return false;
6005 }
6006
6007 $is_anonymous_ms = $this->_storage->get( 'is_anonymous_ms' );
6008
6009 if ( empty( $is_anonymous_ms ) ) {
6010 return false;
6011 }
6012
6013 return $is_anonymous_ms['is'];
6014 }
6015
6016 /**
6017 * Check if super-admin opted-in for all sites in the network.
6018 *
6019 * @author Vova Feldman (@svovaf)
6020 * @since 2.0.0
6021 */
6022 function is_network_connected() {
6023 if ( ! $this->_is_network_active ) {
6024 return false;
6025 }
6026
6027 return $this->_storage->get( 'is_network_connected' );
6028 }
6029
6030 /**
6031 * Check if the user skipped connecting the account with Freemius.
6032 *
6033 * @author Vova Feldman (@svovaf)
6034 * @since 1.0.7
6035 *
6036 * @return bool
6037 */
6038 function is_anonymous() {
6039 if ( ! isset( $this->_is_anonymous ) ) {
6040 if ( $this->is_network_anonymous() ) {
6041 $this->_is_anonymous = true;
6042 } else if ( ! fs_is_network_admin() ) {
6043 if ( ! isset( $this->_storage->is_anonymous ) ) {
6044 // Not skipped.
6045 $this->_is_anonymous = false;
6046 } else if ( is_bool( $this->_storage->is_anonymous ) ) {
6047 // For back compatibility, since the variable was boolean before.
6048 $this->_is_anonymous = $this->_storage->is_anonymous;
6049
6050 // Upgrade stored data format to 1.1.3 format.
6051 $this->set_anonymous_mode( $this->_storage->is_anonymous );
6052 } else {
6053 // Version 1.1.3 and later.
6054 $this->_is_anonymous = $this->_storage->is_anonymous['is'];
6055 }
6056 }
6057 }
6058
6059 return $this->_is_anonymous;
6060 }
6061
6062 /**
6063 * Check if the user skipped the connection of a specified site.
6064 *
6065 * @author Vova Feldman (@svovaf)
6066 * @since 2.0.0
6067 *
6068 * @param int $blog_id
6069 *
6070 * @return bool
6071 */
6072 function is_anonymous_site( $blog_id = 0 ) {
6073 if ( $this->is_network_anonymous() ) {
6074 return true;
6075 }
6076
6077 $is_anonymous = $this->_storage->get( 'is_anonymous', false, $blog_id );
6078
6079 if ( empty( $is_anonymous ) ) {
6080 return false;
6081 }
6082
6083 return $is_anonymous['is'];
6084 }
6085
6086 /**
6087 * Check if user connected his account and install pending email activation.
6088 *
6089 * @author Vova Feldman (@svovaf)
6090 * @since 1.0.7
6091 *
6092 * @return bool
6093 */
6094 function is_pending_activation() {
6095 return $this->_storage->get( 'is_pending_activation', false );
6096 }
6097
6098 /**
6099 * Check if plugin must be WordPress.org compliant.
6100 *
6101 * @since 1.0.7
6102 *
6103 * @return bool
6104 */
6105 function is_org_repo_compliant() {
6106 return $this->_is_org_compliant;
6107 }
6108
6109 #--------------------------------------------------------------------------------
6110 #region WP Cron Common
6111 #--------------------------------------------------------------------------------
6112
6113 /**
6114 * @author Vova Feldman (@svovaf)
6115 * @since 2.0.0
6116 *
6117 * @param string $name Cron name.
6118 *
6119 * @return object
6120 */
6121 private function get_cron_data( $name ) {
6122 $this->_logger->entrance( $name );
6123
6124 /**
6125 * @var object $cron_data
6126 */
6127 return $this->_storage->get( "{$name}_cron", null );
6128 }
6129
6130 /**
6131 * @author Vova Feldman (@svovaf)
6132 * @since 2.0.0
6133 *
6134 * @param string $name Cron name.
6135 */
6136 private function clear_cron_data( $name ) {
6137 $this->_logger->entrance( $name );
6138
6139 $this->_storage->remove( "{$name}_cron" );
6140 }
6141
6142 /**
6143 * @author Vova Feldman (@svovaf)
6144 * @since 2.0.0
6145 *
6146 * @param string $name Cron name.
6147 * @param int $cron_blog_id The cron executing blog ID.
6148 */
6149 private function set_cron_data( $name, $cron_blog_id = 0 ) {
6150 $this->_logger->entrance( $name );
6151
6152 $this->_storage->store( "{$name}_cron", (object) array(
6153 'version' => $this->get_plugin_version(),
6154 'blog_id' => $cron_blog_id,
6155 'sdk_version' => $this->version,
6156 'timestamp' => WP_FS__SCRIPT_START_TIME,
6157 'on' => true,
6158 ) );
6159 }
6160
6161 /**
6162 * Get the cron's executing blog ID.
6163 *
6164 * @author Vova Feldman (@svovaf)
6165 * @since 2.0.0
6166 *
6167 * @param string $name Cron name.
6168 *
6169 * @return int
6170 */
6171 private function get_cron_blog_id( $name ) {
6172 $this->_logger->entrance( $name );
6173
6174 /**
6175 * @var object $cron_data
6176 */
6177 $cron_data = $this->get_cron_data( $name );
6178
6179 return ( is_object( $cron_data ) && is_numeric( $cron_data->blog_id ) ) ?
6180 $cron_data->blog_id :
6181 0;
6182 }
6183
6184 /**
6185 * @author Vova Feldman (@svovaf)
6186 * @since 2.0.0
6187 *
6188 * @param string $name Cron name.
6189 *
6190 * @return bool
6191 */
6192 private function is_cron_on( $name ) {
6193 $this->_logger->entrance( $name );
6194
6195 /**
6196 * @var object $cron_data
6197 */
6198 $cron_data = $this->get_cron_data( $name );
6199
6200 return ( ! is_null( $cron_data ) && true === $cron_data->on );
6201 }
6202
6203 /**
6204 * Unix timestamp for previous cron execution or false if never executed.
6205 *
6206 * @author Vova Feldman (@svovaf)
6207 * @since 2.0.0
6208 *
6209 * @param string $name Cron name.
6210 *
6211 * @return int|false
6212 */
6213 private function cron_last_execution( $name ) {
6214 $this->_logger->entrance( $name );
6215
6216 return $this->_storage->get( "{$name}_timestamp" );
6217 }
6218
6219 /**
6220 * Set cron execution time to now.
6221 *
6222 * @author Vova Feldman (@svovaf)
6223 * @since 2.0.0
6224 *
6225 * @param string $name Cron name.
6226 */
6227 private function set_cron_execution_timestamp( $name ) {
6228 $this->_logger->entrance( $name );
6229
6230 $this->_storage->store( "{$name}_timestamp", time() );
6231 }
6232
6233 /**
6234 * Sets the keepalive time to now.
6235 *
6236 * @author Leo Fajardo (@leorw)
6237 * @since 2.2.3
6238 *
6239 * @param bool|null $use_network_level_storage
6240 */
6241 private function set_keepalive_timestamp( $use_network_level_storage = null ) {
6242 $this->_logger->entrance();
6243
6244 $this->_storage->store( 'keepalive_timestamp', time(), $use_network_level_storage );
6245 }
6246
6247 /**
6248 * Check if cron was executed in the last $period of seconds.
6249 *
6250 * @author Vova Feldman (@svovaf)
6251 * @since 2.0.0
6252 *
6253 * @param string $name Cron name.
6254 * @param int $period In seconds
6255 *
6256 * @return bool
6257 */
6258 private function is_cron_executed( $name, $period = WP_FS__TIME_24_HOURS_IN_SEC ) {
6259 $this->_logger->entrance( $name );
6260
6261 $last_execution = $this->cron_last_execution( $name );
6262
6263 if ( ! is_numeric( $last_execution ) ) {
6264 return false;
6265 }
6266
6267 return ( $last_execution > ( WP_FS__SCRIPT_START_TIME - $period ) );
6268 }
6269
6270 /**
6271 * WP Cron is executed on a site level. When running in a multisite network environment
6272 * with the network integration activated, for optimization reasons, we are consolidating
6273 * the installs data sync cron to be executed only from a single site.
6274 *
6275 * @author Vova Feldman (@svovaf)
6276 * @since 2.0.0
6277 *
6278 * @param int $except_blog_id Target any except the excluded blog ID.
6279 *
6280 * @return int
6281 */
6282 private function get_cron_target_blog_id( $except_blog_id = 0 ) {
6283 if ( ! is_multisite() ) {
6284 return 0;
6285 }
6286
6287 if ( $this->_is_network_active &&
6288 is_numeric( $this->_storage->network_install_blog_id ) &&
6289 $except_blog_id != $this->_storage->network_install_blog_id &&
6290 self::is_site_active( $this->_storage->network_install_blog_id )
6291 ) {
6292 // Try to run cron from the main network blog.
6293 $install = $this->get_install_by_blog_id( $this->_storage->network_install_blog_id );
6294
6295 if ( is_object( $install ) &&
6296 ( $this->is_premium() || $install->is_tracking_allowed() )
6297 ) {
6298 return $this->_storage->network_install_blog_id;
6299 }
6300 }
6301
6302 // Get first opted-in blog ID with active tracking.
6303 $installs = $this->get_blog_install_map();
6304 foreach ( $installs as $blog_id => $install ) {
6305 if ( $except_blog_id != $blog_id &&
6306 self::is_site_active( $blog_id ) &&
6307 ( $this->is_premium() || $install->is_tracking_allowed() )
6308 ) {
6309 return $blog_id;
6310 }
6311 }
6312
6313 return 0;
6314 }
6315
6316 /**
6317 * @author Vova Feldman (@svovaf)
6318 * @since 2.0.0
6319 *
6320 * @param string $name Cron name.
6321 * @param string $action_tag Callback action tag.
6322 * @param bool $is_network_clear If set to TRUE, clear sync cron even if there are installs that are still connected.
6323 */
6324 private function clear_cron( $name, $action_tag = '', $is_network_clear = false ) {
6325 $this->_logger->entrance( $name );
6326
6327 if ( ! $this->is_cron_on( $name ) ) {
6328 return;
6329 }
6330
6331 $clear_cron = true;
6332 if ( ! $is_network_clear && $this->_is_network_active ) {
6333 $installs = $this->get_blog_install_map();
6334
6335 foreach ( $installs as $blog_id => $install ) {
6336 /**
6337 * @var FS_Site $install
6338 */
6339 if ( $install->is_tracking_allowed() ) {
6340 $clear_cron = false;
6341 break;
6342 }
6343 }
6344 }
6345
6346 if ( ! $clear_cron ) {
6347 return;
6348 }
6349
6350 /**
6351 * @var object $cron_data
6352 */
6353 $cron_data = $this->get_cron_data( $name );
6354
6355 $cron_blog_id = is_object( $cron_data ) && isset( $cron_data->blog_id ) ?
6356 $cron_data->blog_id :
6357 0;
6358
6359 $this->clear_cron_data( $name );
6360
6361 if ( 0 < $cron_blog_id ) {
6362 switch_to_blog( $cron_blog_id );
6363 }
6364
6365 if ( empty( $action_tag ) ) {
6366 $action_tag = $name;
6367 }
6368
6369 wp_clear_scheduled_hook( $this->get_action_tag( $action_tag ) );
6370
6371 if ( 0 < $cron_blog_id ) {
6372 restore_current_blog();
6373 }
6374 }
6375
6376 /**
6377 * Unix timestamp for next cron execution or false if not scheduled.
6378 *
6379 * @author Vova Feldman (@svovaf)
6380 * @since 2.0.0
6381 *
6382 * @param string $name Cron name.
6383 * @param string $action_tag Callback action tag.
6384 *
6385 * @return int|false
6386 */
6387 private function get_next_scheduled_cron( $name, $action_tag = '' ) {
6388 $this->_logger->entrance( $name );
6389
6390 if ( ! $this->is_cron_on( $name ) ) {
6391 return false;
6392 }
6393
6394 /**
6395 * @var object $cron_data
6396 */
6397 $cron_data = $this->get_cron_data( $name );
6398
6399 $cron_blog_id = is_object( $cron_data ) && isset( $cron_data->blog_id ) ?
6400 $cron_data->blog_id :
6401 0;
6402
6403 if ( 0 < $cron_blog_id ) {
6404 switch_to_blog( $cron_blog_id );
6405 }
6406
6407 if ( empty( $action_tag ) ) {
6408 $action_tag = $name;
6409 }
6410
6411 $next_scheduled = wp_next_scheduled( $this->get_action_tag( $action_tag ) );
6412
6413 if ( 0 < $cron_blog_id ) {
6414 restore_current_blog();
6415 }
6416
6417 return $next_scheduled;
6418 }
6419
6420 /**
6421 * @author Vova Feldman (@svovaf)
6422 * @since 2.0.0
6423 *
6424 * @param string $name Cron name.
6425 * @param string $action_tag Callback action tag.
6426 * @param string $recurrence 'single' or 'daily'.
6427 * @param int $start_at Defaults to now.
6428 * @param bool $randomize_start If true, schedule first job randomly during the next 12 hours. Otherwise, schedule job to start right away.
6429 * @param int $except_blog_id Target any except the excluded blog ID.
6430 */
6431 private function schedule_cron(
6432 $name,
6433 $action_tag = '',
6434 $recurrence = 'single',
6435 $start_at = WP_FS__SCRIPT_START_TIME,
6436 $randomize_start = true,
6437 $except_blog_id = 0
6438 ) {
6439 $this->_logger->entrance( $name );
6440
6441 $this->clear_cron( $name, $action_tag, true );
6442
6443 $cron_blog_id = $this->get_cron_target_blog_id( $except_blog_id );
6444
6445 if ( is_multisite() && 0 == $cron_blog_id ) {
6446 // Don't schedule cron since couldn't find a target blog.
6447 return;
6448 }
6449
6450 if ( 0 < $cron_blog_id ) {
6451 switch_to_blog( $cron_blog_id );
6452 }
6453
6454 if ( 'daily' === $recurrence ) {
6455 if ( $randomize_start ) {
6456 // Schedule first sync with a random 12 hour time range from now.
6457 $start_at += rand( 0, ( WP_FS__TIME_24_HOURS_IN_SEC / 2 ) );
6458 }
6459
6460 // Schedule daily WP cron.
6461 wp_schedule_event(
6462 $start_at,
6463 'daily',
6464 $this->get_action_tag( $action_tag )
6465 );
6466 } else if ( 'single' === $recurrence ) {
6467 // Schedule single cron.
6468 wp_schedule_single_event(
6469 $start_at,
6470 $this->get_action_tag( $action_tag )
6471 );
6472 }
6473
6474 $this->set_cron_data( $name, $cron_blog_id );
6475
6476 if ( 0 < $cron_blog_id ) {
6477 restore_current_blog();
6478 }
6479 }
6480
6481 /**
6482 * Consolidated cron execution for performance optimization. The max number of API requests is based on the number of unique opted-in users.
6483 * that doesn't halt page loading.
6484 *
6485 * @author Vova Feldman (@svovaf)
6486 * @since 2.0.0
6487 *
6488 * @param string $name Cron name.
6489 * @param callable $callable The function that should be executed.
6490 */
6491 private function execute_cron( $name, $callable ) {
6492 $this->_logger->entrance( $name );
6493
6494 // Store the last time data sync was executed.
6495 $this->set_cron_execution_timestamp( $name );
6496
6497 // Check if API is temporary down.
6498 if ( FS_Api::is_temporary_down() ) {
6499 return;
6500 }
6501
6502 // @todo Add logic that identifies API latency, and reschedule the next background sync randomly between 8-16 hours.
6503
6504 $users_2_blog_ids = array();
6505
6506 if ( ! is_multisite() ) {
6507 // Add dummy blog.
6508 $users_2_blog_ids[0] = array( 0 );
6509 } else {
6510 $installs = $this->get_blog_install_map();
6511 foreach ( $installs as $blog_id => $install ) {
6512 if ( $this->is_premium() || $install->is_tracking_allowed() ) {
6513 if ( ! isset( $users_2_blog_ids[ $install->user_id ] ) ) {
6514 $users_2_blog_ids[ $install->user_id ] = array();
6515 }
6516
6517 $users_2_blog_ids[ $install->user_id ][] = $blog_id;
6518 }
6519 }
6520 }
6521
6522 $current_blog_id = get_current_blog_id();
6523
6524 foreach ( $users_2_blog_ids as $user_id => $blog_ids ) {
6525 if ( 0 < $blog_ids[0] ) {
6526 $this->switch_to_blog( $blog_ids[0] );
6527 }
6528
6529 call_user_func_array( $callable, array( $blog_ids, ( is_multisite() ? $current_blog_id : null ) ) );
6530
6531 foreach ( $blog_ids as $blog_id ) {
6532 $this->do_action( "after_{$name}_cron", $blog_id );
6533 }
6534 }
6535
6536 if ( is_multisite() ) {
6537 $this->switch_to_blog( $current_blog_id, fs_is_network_admin() ? $this->get_network_install() : null );
6538
6539 $this->do_action( "after_{$name}_cron_multisite" );
6540 }
6541 }
6542
6543 #endregion
6544
6545 #----------------------------------------------------------------------------------
6546 #region Daily Sync Cron
6547 #----------------------------------------------------------------------------------
6548
6549
6550 /**
6551 * @author Vova Feldman (@svovaf)
6552 * @since 2.0.0
6553 *
6554 * @return bool
6555 */
6556 private function is_sync_cron_scheduled() {
6557 return $this->is_cron_on( 'sync' );
6558 }
6559
6560 /**
6561 * Get the sync cron's executing blog ID.
6562 *
6563 * @author Vova Feldman (@svovaf)
6564 * @since 2.0.0
6565 *
6566 * @return int
6567 */
6568 private function get_sync_cron_blog_id() {
6569 return $this->get_cron_blog_id( 'sync' );
6570 }
6571
6572 /**
6573 * @author Vova Feldman (@svovaf)
6574 * @since 1.1.7.3
6575 */
6576 private function run_manual_sync() {
6577 self::require_pluggable_essentials();
6578
6579 if ( ! $this->is_user_admin() ) {
6580 return;
6581 }
6582
6583 // Run manual sync.
6584 $this->_sync_cron();
6585
6586 // Reschedule next cron to run 24 hours from now (performance optimization).
6587 $this->schedule_sync_cron( time() + WP_FS__TIME_24_HOURS_IN_SEC, false );
6588 }
6589
6590 /**
6591 * Data sync cron job. Replaces the background sync non blocking HTTP request
6592 * that doesn't halt page loading.
6593 *
6594 * @author Vova Feldman (@svovaf)
6595 * @since 1.1.7.3
6596 * @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.
6597 */
6598 function _sync_cron() {
6599 $this->_logger->entrance();
6600
6601 $this->execute_cron( 'sync', array( &$this, '_sync_cron_method' ) );
6602 }
6603
6604 /**
6605 * The actual data sync cron logic.
6606 *
6607 * @author Vova Feldman (@svovaf)
6608 * @since 2.0.0
6609 *
6610 * @param int[] $blog_ids
6611 * @param int|null $current_blog_id @since 2.2.3. This is passed from the `execute_cron` method and used by the
6612 * `_sync_plugin_license` method in order to switch to the previous blog when sending
6613 * updates for a single site in case `execute_cron` has switched to a different blog.
6614 */
6615 function _sync_cron_method( array $blog_ids, $current_blog_id = null ) {
6616 if ( $this->is_registered() ) {
6617 $this->sync_user_beta_mode();
6618
6619 if ( $this->has_paid_plan() ) {
6620 // Initiate background plan sync.
6621 $this->_sync_license( true, false, $current_blog_id );
6622
6623 if ( $this->is_paying() ) {
6624 // Check for premium plugin updates.
6625 $this->check_updates( true );
6626 }
6627 } else {
6628 // Sync install(s) (only if something changed locally).
6629 if ( 1 < count( $blog_ids ) ) {
6630 $this->sync_installs();
6631 } else {
6632 $this->sync_install();
6633 }
6634 }
6635 }
6636 }
6637
6638 /**
6639 * Check if sync was executed in the last $period of seconds.
6640 *
6641 * @author Vova Feldman (@svovaf)
6642 * @since 1.1.7.3
6643 *
6644 * @param int $period In seconds
6645 *
6646 * @return bool
6647 */
6648 private function is_sync_executed( $period = WP_FS__TIME_24_HOURS_IN_SEC ) {
6649 return $this->is_cron_executed( 'sync', $period );
6650 }
6651
6652 /**
6653 * @author Vova Feldman (@svovaf)
6654 * @since 1.1.7.3
6655 *
6656 * @return bool
6657 */
6658 private function is_sync_cron_on() {
6659 return $this->is_cron_on( 'sync' );
6660 }
6661
6662 /**
6663 * @author Vova Feldman (@svovaf)
6664 * @since 1.1.7.3
6665 *
6666 * @param int $start_at Defaults to now.
6667 * @param bool $randomize_start If true, schedule first job randomly during the next 12 hours. Otherwise, schedule job to start right away.
6668 * @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.
6669 */
6670 private function schedule_sync_cron(
6671 $start_at = WP_FS__SCRIPT_START_TIME,
6672 $randomize_start = true,
6673 $except_blog_id = 0
6674 ) {
6675 $this->schedule_cron(
6676 'sync',
6677 'data_sync',
6678 'daily',
6679 $start_at,
6680 $randomize_start,
6681 $except_blog_id
6682 );
6683 }
6684
6685 /**
6686 * Add the actual sync function to the cron job hook.
6687 *
6688 * @author Vova Feldman (@svovaf)
6689 * @since 1.1.7.3
6690 */
6691 private function hook_callback_to_sync_cron() {
6692 $this->add_action( 'data_sync', array( &$this, '_sync_cron' ) );
6693 }
6694
6695 /**
6696 * @author Vova Feldman (@svovaf)
6697 * @since 1.1.7.3
6698 *
6699 * @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.
6700 */
6701 private function clear_sync_cron( $is_network_clear = false ) {
6702 $this->_logger->entrance();
6703
6704 $this->clear_cron( 'sync', 'data_sync', $is_network_clear );
6705 }
6706
6707 /**
6708 * Unix timestamp for next sync cron execution or false if not scheduled.
6709 *
6710 * @author Vova Feldman (@svovaf)
6711 * @since 1.1.7.3
6712 *
6713 * @return int|false
6714 */
6715 function next_sync_cron() {
6716 return $this->get_next_scheduled_cron( 'sync', 'data_sync' );
6717 }
6718
6719 /**
6720 * Unix timestamp for previous sync cron execution or false if never executed.
6721 *
6722 * @author Vova Feldman (@svovaf)
6723 * @since 1.1.7.3
6724 *
6725 * @return int|false
6726 */
6727 function last_sync_cron() {
6728 return $this->cron_last_execution( 'sync' );
6729 }
6730
6731 #endregion Daily Sync Cron ------------------------------------------------------------------
6732
6733 #----------------------------------------------------------------------------------
6734 #region Async Install Sync
6735 #----------------------------------------------------------------------------------
6736
6737 /**
6738 * @author Vova Feldman (@svovaf)
6739 * @since 1.1.7.3
6740 *
6741 * @return bool
6742 */
6743 private function is_install_sync_scheduled() {
6744 return $this->is_cron_on( 'install_sync' );
6745 }
6746
6747 /**
6748 * Get the sync cron's executing blog ID.
6749 *
6750 * @author Vova Feldman (@svovaf)
6751 * @since 2.0.0
6752 *
6753 * @return int
6754 */
6755 private function get_install_sync_cron_blog_id() {
6756 return $this->get_cron_blog_id( 'install_sync' );
6757 }
6758
6759 /**
6760 * Instead of running blocking install sync event, execute non blocking scheduled wp-cron.
6761 *
6762 * @author Vova Feldman (@svovaf)
6763 * @since 1.1.7.3
6764 *
6765 * @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.
6766 */
6767 private function schedule_install_sync( $except_blog_id = 0 ) {
6768 $this->schedule_cron( 'install_sync', 'install_sync', 'single', WP_FS__SCRIPT_START_TIME, false, $except_blog_id );
6769 }
6770
6771 /**
6772 * Unix timestamp for previous install sync cron execution or false if never executed.
6773 *
6774 * @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.
6775 *
6776 * @author Vova Feldman (@svovaf)
6777 * @since 1.1.7.3
6778 *
6779 * @return int|false
6780 */
6781 function last_install_sync() {
6782 return $this->cron_last_execution( 'install_sync' );
6783 }
6784
6785 /**
6786 * Unix timestamp for next install sync cron execution or false if not scheduled.
6787 *
6788 * @author Vova Feldman (@svovaf)
6789 * @since 1.1.7.3
6790 *
6791 * @return int|false
6792 */
6793 function next_install_sync() {
6794 return $this->get_next_scheduled_cron( 'install_sync', 'install_sync' );
6795 }
6796
6797 /**
6798 * Add the actual install sync function to the cron job hook.
6799 *
6800 * @author Vova Feldman (@svovaf)
6801 * @since 1.1.7.3
6802 */
6803 private function hook_callback_to_install_sync() {
6804 $this->add_action( 'install_sync', array( &$this, '_run_sync_install' ) );
6805 }
6806
6807 /**
6808 * @author Vova Feldman (@svovaf)
6809 * @since 1.1.7.3
6810 *
6811 * @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.
6812 */
6813 private function clear_install_sync_cron( $is_network_clear = false ) {
6814 $this->_logger->entrance();
6815
6816 $this->clear_cron( 'install_sync', 'install_sync', $is_network_clear );
6817 }
6818
6819 /**
6820 * @author Vova Feldman (@svovaf)
6821 * @since 1.1.7.3
6822 * @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.
6823 */
6824 public function _run_sync_install() {
6825 $this->_logger->entrance();
6826
6827 $this->execute_cron( 'sync', array( &$this, '_sync_install_cron_method' ) );
6828 }
6829
6830 /**
6831 * The actual install(s) sync cron logic.
6832 *
6833 * @author Vova Feldman (@svovaf)
6834 * @since 2.0.0
6835 *
6836 * @param int[] $blog_ids
6837 * @param int|null $current_blog_id
6838 */
6839 function _sync_install_cron_method( array $blog_ids, $current_blog_id = null ) {
6840 if ( $this->is_registered() ) {
6841 if ( 1 < count( $blog_ids ) ) {
6842 $this->sync_installs( array(), true );
6843 } else {
6844 $this->sync_install( array(), true );
6845 }
6846 }
6847 }
6848
6849 #endregion Async Install Sync ------------------------------------------------------------------
6850
6851 /**
6852 * Show a notice that activation is currently pending.
6853 *
6854 * @author Vova Feldman (@svovaf)
6855 * @since 1.0.7
6856 *
6857 * @param bool|string $email
6858 * @param bool $is_pending_trial Since 1.2.1.5
6859 */
6860 function _add_pending_activation_notice( $email = false, $is_pending_trial = false ) {
6861 if ( ! is_string( $email ) ) {
6862 $current_user = self::_get_current_wp_user();
6863 $email = $current_user->user_email;
6864 }
6865
6866 $this->_admin_notices->add_sticky(
6867 sprintf(
6868 $this->get_text_inline( 'You should receive an activation email for %s to your mailbox at %s. Please make sure you click the activation button in that email to %s.', 'pending-activation-message' ),
6869 '<b>' . $this->get_plugin_name() . '</b>',
6870 '<b>' . $email . '</b>',
6871 ( $is_pending_trial ?
6872 $this->get_text_inline( 'start the trial', 'start-the-trial' ) :
6873 $this->get_text_inline( 'complete the install', 'complete-the-install' ) )
6874 ),
6875 'activation_pending',
6876 'Thanks!'
6877 );
6878 }
6879
6880 /**
6881 * Check if currently in plugin activation.
6882 *
6883 * @author Vova Feldman (@svovaf)
6884 * @since 1.1.4
6885 *
6886 * @return bool
6887 */
6888 function is_plugin_activation() {
6889 $result = get_transient( "fs_{$this->_module_type}_{$this->_slug}_activated" );
6890
6891 return !empty($result);
6892 }
6893
6894 /**
6895 *
6896 * NOTE: admin_menu action executed before admin_init.
6897 *
6898 * @author Vova Feldman (@svovaf)
6899 * @since 1.0.7
6900 */
6901 function _admin_init_action() {
6902 /**
6903 * Automatically redirect to connect/activation page after plugin activation.
6904 *
6905 * @since 1.1.7 Do NOT redirect to opt-in when running in network admin mode.
6906 */
6907 if ( $this->is_plugin_activation() ) {
6908 delete_transient( "fs_{$this->_module_type}_{$this->_slug}_activated" );
6909
6910 if ( isset( $_GET['activate-multi'] ) ) {
6911 /**
6912 * Don't redirect if activating multiple plugins at once (bulk activation).
6913 */
6914 } else {
6915 $this->_redirect_on_activation_hook();
6916 return;
6917 }
6918 }
6919
6920 if ( fs_request_is_action( $this->get_unique_affix() . '_skip_activation' ) ) {
6921 check_admin_referer( $this->get_unique_affix() . '_skip_activation' );
6922
6923 $this->skip_connection( null, fs_is_network_admin() );
6924
6925 fs_redirect( $this->get_after_activation_url( 'after_skip_url' ) );
6926 }
6927
6928 if ( $this->is_network_activation_mode() &&
6929 fs_request_is_action( $this->get_unique_affix() . '_delegate_activation' )
6930 ) {
6931 check_admin_referer( $this->get_unique_affix() . '_delegate_activation' );
6932
6933 $this->delegate_connection();
6934
6935 fs_redirect( $this->get_after_activation_url( 'after_delegation_url' ) );
6936 }
6937
6938 $this->_add_upgrade_action_link();
6939
6940 if ( ! $this->is_addon() &&
6941 ! ( ! $this->_is_network_active && fs_is_network_admin() ) &&
6942 (
6943 ( true === $this->_storage->require_license_activation ) ||
6944 // Not registered nor anonymous.
6945 ( ! $this->is_registered() && ! $this->is_anonymous() ) ||
6946 // OR, network level and in network upgrade mode.
6947 ( fs_is_network_admin() && $this->_is_network_active && $this->is_network_upgrade_mode() )
6948 )
6949 ) {
6950 if ( ! $this->is_pending_activation() ) {
6951 if ( ! $this->_menu->is_main_settings_page() ) {
6952 /**
6953 * If a user visits any other admin page before activating the premium-only theme with a valid
6954 * license, reactivate the previous theme.
6955 *
6956 * @author Leo Fajardo (@leorw)
6957 * @since 1.2.2
6958 */
6959 if ( $this->is_theme() &&
6960 ! $this->has_settings_menu() &&
6961 ! isset( $_REQUEST['fs_action'] ) &&
6962 $this->can_activate_previous_theme()
6963 ) {
6964 if ( $this->is_only_premium() ) {
6965 $this->activate_previous_theme();
6966 return;
6967 }
6968
6969 if ( true === $this->_storage->require_license_activation ) {
6970 $this->_storage->require_license_activation = false;
6971 }
6972 }
6973
6974 if ( ! fs_is_network_admin() &&
6975 $this->is_network_activation_mode() &&
6976 ! $this->is_delegated_connection()
6977 ) {
6978 return;
6979 }
6980
6981 if ( $this->is_plugin_new_install() || $this->is_only_premium() ) {
6982 if ( ! $this->_anonymous_mode ) {
6983 // Show notice for new plugin installations.
6984 $this->_admin_notices->add(
6985 sprintf(
6986 $this->get_text_inline( 'You are just one step away - %s', 'you-are-step-away' ),
6987 sprintf( '<b><a href="%s">%s</a></b>',
6988 $this->get_activation_url( array(), ! $this->is_delegated_connection() ),
6989 sprintf( $this->get_text_x_inline( 'Complete "%s" Activation Now',
6990 '%s - plugin name. As complete "PluginX" activation now', 'activate-x-now' ), $this->get_plugin_name() )
6991 )
6992 ),
6993 '',
6994 'update-nag'
6995 );
6996 }
6997 } else {
6998 if ( $this->should_add_sticky_optin_notice() ) {
6999 $this->add_sticky_optin_admin_notice();
7000 }
7001
7002 if ( $this->has_filter( 'optin_pointer_element' ) ) {
7003 // Don't show admin nag if plugin update.
7004 wp_enqueue_script( 'wp-pointer' );
7005 wp_enqueue_style( 'wp-pointer' );
7006
7007 $this->_enqueue_connect_essentials();
7008
7009 add_action( 'admin_print_footer_scripts', array(
7010 $this,
7011 '_add_connect_pointer_script'
7012 ) );
7013 }
7014 }
7015 }
7016 }
7017
7018 if ( $this->is_theme() &&
7019 $this->_menu->is_main_settings_page()
7020 ) {
7021 $this->_show_theme_activation_optin_dialog();
7022 }
7023 }
7024 }
7025
7026 /**
7027 * @author Vova Feldman (@svovaf)
7028 * @since 2.0.0
7029 *
7030 * @return bool
7031 */
7032 private function should_add_sticky_optin_notice() {
7033 if ( fs_is_network_admin() ) {
7034 if ( ! $this->_is_network_active ) {
7035 return false;
7036 }
7037
7038 if ( ! $this->is_network_activation_mode() ) {
7039 return false;
7040 }
7041
7042 return ! isset( $this->_storage->sticky_optin_added_ms );
7043 }
7044
7045 if ( ! $this->is_activation_mode() ) {
7046 return false;
7047 }
7048
7049 // If running from a blog admin and delegated the connection.
7050 return ! isset( $this->_storage->sticky_optin_added );
7051 }
7052
7053 /**
7054 * @author Leo Fajardo (@leorw)
7055 * @since 2.0.0
7056 */
7057 private function add_sticky_optin_admin_notice() {
7058 if ( ! $this->_is_network_active || ! fs_is_network_admin() ) {
7059 $this->_storage->sticky_optin_added = true;
7060 } else {
7061 $this->_storage->sticky_optin_added_ms = true;
7062 }
7063
7064 // Show notice for new plugin installations.
7065 $this->_admin_notices->add_sticky(
7066 sprintf(
7067 $this->get_text_inline( 'We made a few tweaks to the %s, %s', 'few-plugin-tweaks' ),
7068 $this->_module_type,
7069 sprintf( '<b><a href="%s">%s</a></b>',
7070 $this->get_activation_url(),
7071 sprintf( $this->get_text_inline( 'Opt in to make "%s" better!', 'optin-x-now' ), $this->get_plugin_name() )
7072 )
7073 ),
7074 'connect_account',
7075 '',
7076 'update-nag'
7077 );
7078 }
7079
7080 /**
7081 * Enqueue connect requires scripts and styles.
7082 *
7083 * @author Vova Feldman (@svovaf)
7084 * @since 1.1.4
7085 */
7086 function _enqueue_connect_essentials() {
7087 wp_enqueue_script( 'jquery' );
7088 wp_enqueue_script( 'json2' );
7089
7090 fs_enqueue_local_script( 'postmessage', 'nojquery.ba-postmessage.min.js' );
7091 fs_enqueue_local_script( 'fs-postmessage', 'postmessage.js' );
7092
7093 fs_enqueue_local_style( 'fs_connect', '/admin/connect.css' );
7094 }
7095
7096 /**
7097 * Add connect / opt-in pointer.
7098 *
7099 * @author Vova Feldman (@svovaf)
7100 * @since 1.1.4
7101 */
7102 function _add_connect_pointer_script() {
7103 $vars = array( 'id' => $this->_module_id );
7104 $pointer_content = fs_get_template( 'connect.php', $vars );
7105 ?>
7106 <script type="text/javascript">// <![CDATA[
7107 jQuery(document).ready(function ($) {
7108 if ('undefined' !== typeof(jQuery().pointer)) {
7109
7110 var element = <?php echo $this->apply_filters( 'optin_pointer_element', '$("#non_existing_element");' ) ?>;
7111
7112 if (element.length > 0) {
7113 var optin = $(element).pointer($.extend(true, {}, {
7114 content : <?php echo json_encode( $pointer_content ) ?>,
7115 position : {
7116 edge : 'left',
7117 align: 'center'
7118 },
7119 buttons : function () {
7120 // Don't show pointer buttons.
7121 return '';
7122 },
7123 pointerWidth: 482
7124 }, <?php echo $this->apply_filters( 'optin_pointer_options_json', '{}' ) ?>));
7125
7126 <?php
7127 echo $this->apply_filters( 'optin_pointer_execute', "
7128
7129 optin.pointer('open');
7130
7131 // Tag the opt-in pointer with custom class.
7132 $('.wp-pointer #fs_connect')
7133 .parents('.wp-pointer.wp-pointer-top')
7134 .addClass('fs-opt-in-pointer');
7135
7136 ", 'element', 'optin' ) ?>
7137 }
7138 }
7139 });
7140 // ]]></script>
7141 <?php
7142 }
7143
7144 /**
7145 * Return current page's URL.
7146 *
7147 * @author Vova Feldman (@svovaf)
7148 * @since 1.0.7
7149 *
7150 * @return string
7151 */
7152 function current_page_url() {
7153 $url = 'http';
7154
7155 if ( isset( $_SERVER["HTTPS"] ) ) {
7156 if ( $_SERVER["HTTPS"] == "on" ) {
7157 $url .= "s";
7158 }
7159 }
7160 $url .= "://";
7161 if ( $_SERVER["SERVER_PORT"] != "80" ) {
7162 $url .= $_SERVER["SERVER_NAME"] . ":" . $_SERVER["SERVER_PORT"] . $_SERVER["REQUEST_URI"];
7163 } else {
7164 $url .= $_SERVER["SERVER_NAME"] . $_SERVER["REQUEST_URI"];
7165 }
7166
7167 return esc_url( $url );
7168 }
7169
7170 /**
7171 * Check if the current page is the plugin's main admin settings page.
7172 *
7173 * @author Vova Feldman (@svovaf)
7174 * @since 1.0.7
7175 *
7176 * @return bool
7177 */
7178 function _is_plugin_page() {
7179 return fs_is_plugin_page( $this->_menu->get_raw_slug() ) ||
7180 fs_is_plugin_page( $this->_slug );
7181 }
7182
7183 /* Events
7184 ------------------------------------------------------------------------------------------------------------------*/
7185 /**
7186 * Delete site install from Database.
7187 *
7188 * @author Vova Feldman (@svovaf)
7189 * @since 1.0.1
7190 *
7191 * @param bool $store
7192 * @param int|null $blog_id Since 2.0.0
7193 *
7194 * @return false|int The install ID if deleted. Otherwise, FALSE (when install not exist).
7195 */
7196 function _delete_site( $store = true, $blog_id = null ) {
7197 return self::_delete_site_by_slug( $this->_slug, $this->_module_type, $store, $blog_id );
7198 }
7199
7200 /**
7201 * Delete site install from Database.
7202 *
7203 * @author Vova Feldman (@svovaf)
7204 * @since 1.2.2.7
7205 *
7206 * @param string $slug
7207 * @param string $module_type
7208 * @param bool $store
7209 * @param int|null $blog_id Since 2.0.0
7210 *
7211 * @return false|int The install ID if deleted. Otherwise, FALSE (when install not exist).
7212 */
7213 static function _delete_site_by_slug( $slug, $module_type, $store = true, $blog_id = null ) {
7214 $sites = self::get_all_sites( $module_type, $blog_id );
7215
7216 $install_id = false;
7217
7218 if ( isset( $sites[ $slug ] ) ) {
7219 if ( is_object( $sites[ $slug ] ) ) {
7220 $install_id = $sites[ $slug ]->id;
7221 }
7222
7223 unset( $sites[ $slug ] );
7224
7225 self::set_account_option_by_module( $module_type, 'sites', $sites, $store, $blog_id );
7226 }
7227
7228 return $install_id;
7229 }
7230
7231 /**
7232 * Delete user.
7233 *
7234 * @author Vova Feldman (@svovaf)
7235 * @since 2.0.0
7236 *
7237 * @param number $user_id
7238 * @param bool $store
7239 *
7240 * @return false|int The user ID if deleted. Otherwise, FALSE (when install not exist).
7241 */
7242 private static function delete_user( $user_id, $store = true ) {
7243 $users = self::get_all_users();
7244
7245 if ( ! is_array( $users ) || ! isset( $users[ $user_id ] ) ) {
7246 return false;
7247 }
7248
7249 unset( $users[ $user_id ] );
7250
7251 self::$_accounts->set_option( 'users', $users, $store );
7252
7253 return $user_id;
7254 }
7255
7256 /**
7257 * Delete plugin's plans information.
7258 *
7259 * @param bool $store Flush to Database if true.
7260 * @param bool $keep_associated_plans If set to false, delete all plans, even if a plan is associated with an install.
7261 *
7262 * @author Vova Feldman (@svovaf)
7263 * @since 1.0.9
7264 */
7265 private function _delete_plans( $store = true, $keep_associated_plans = true ) {
7266 $this->_logger->entrance();
7267
7268 $plans = self::get_all_plans( $this->_module_type );
7269
7270 $plans_to_keep = array();
7271
7272 if ( $keep_associated_plans ) {
7273 $plans_ids_to_keep = $this->get_plans_ids_associated_with_installs();
7274 foreach ( $plans_ids_to_keep as $plan_id ) {
7275 $plan = self::_get_plan_by_id( $plan_id );
7276 if ( is_object( $plan ) ) {
7277 $plans_to_keep[] = self::_encrypt_entity( $plan );
7278 }
7279 }
7280 }
7281
7282 if ( ! empty( $plans_to_keep ) ) {
7283 $plans[ $this->_slug ] = $plans_to_keep;
7284 } else {
7285 unset( $plans[ $this->_slug ] );
7286 }
7287
7288 $this->set_account_option( 'plans', $plans, $store );
7289 }
7290
7291 /**
7292 * Delete all plugin licenses.
7293 *
7294 * @author Vova Feldman (@svovaf)
7295 * @since 1.0.9
7296 *
7297 * @param bool $store
7298 */
7299 private function _delete_licenses( $store = true ) {
7300 $this->_logger->entrance();
7301
7302 $all_licenses = self::get_all_licenses();
7303
7304 unset( $all_licenses[ $this->_module_id ] );
7305
7306 self::$_accounts->set_option( 'all_licenses', $all_licenses, $store );
7307 }
7308
7309 /**
7310 * Check if Freemius was added on new plugin installation.
7311 *
7312 * @author Vova Feldman (@svovaf)
7313 * @since 1.1.5
7314 *
7315 * @return bool
7316 */
7317 function is_plugin_new_install() {
7318 return isset( $this->_storage->is_plugin_new_install ) &&
7319 $this->_storage->is_plugin_new_install;
7320 }
7321
7322 /**
7323 * Check if it's the first plugin release that is running Freemius.
7324 *
7325 * @author Vova Feldman (@svovaf)
7326 * @since 1.2.1.5
7327 *
7328 * @return bool
7329 */
7330 function is_first_freemius_powered_version() {
7331 return empty( $this->_storage->plugin_last_version );
7332 }
7333
7334 /**
7335 * @author Leo Fajardo (@leorw)
7336 * @since 1.2.2
7337 *
7338 * @return bool|string
7339 */
7340 private function get_previous_theme_slug() {
7341 return isset( $this->_storage->previous_theme ) ?
7342 $this->_storage->previous_theme :
7343 false;
7344 }
7345
7346 /**
7347 * @author Leo Fajardo (@leorw)
7348 * @since 1.2.2
7349 *
7350 * @return string
7351 */
7352 private function can_activate_previous_theme() {
7353 $slug = $this->get_previous_theme_slug();
7354 if ( false !== $slug && current_user_can( 'switch_themes' ) ) {
7355 $theme_instance = wp_get_theme( $slug );
7356
7357 return $theme_instance->exists();
7358 }
7359
7360 return false;
7361 }
7362
7363 /**
7364 * @author Leo Fajardo (@leorw)
7365 * @since 1.2.2
7366 */
7367 private function activate_previous_theme() {
7368 switch_theme( $this->get_previous_theme_slug() );
7369 unset( $this->_storage->previous_theme );
7370
7371 global $pagenow;
7372 if ( 'themes.php' === $pagenow ) {
7373 /**
7374 * Refresh the active theme information.
7375 *
7376 * @author Leo Fajardo (@leorw)
7377 * @since 1.2.2
7378 */
7379 fs_redirect( $this->admin_url( $pagenow ) );
7380 }
7381 }
7382
7383 /**
7384 * @author Leo Fajardo (@leorw)
7385 * @since 1.2.2
7386 *
7387 * @return string
7388 */
7389 function get_previous_theme_activation_url() {
7390 if ( ! $this->can_activate_previous_theme() ) {
7391 return '';
7392 }
7393
7394 /**
7395 * Activation URL
7396 *
7397 * @author Leo Fajardo (@leorw)
7398 * @since 1.2.2
7399 */
7400 return wp_nonce_url(
7401 $this->admin_url( 'themes.php?action=activate&stylesheet=' . urlencode( $this->get_previous_theme_slug() ) ),
7402 'switch-theme_' . $this->get_previous_theme_slug()
7403 );
7404 }
7405
7406 /**
7407 * Saves the slug of the previous theme if it still exists so that it can be used by the logic in the opt-in
7408 * form that decides whether to add a close button to the opt-in dialog or not. So after a premium-only theme is
7409 * activated, the close button will appear and will reactivate the previous theme if clicked. If the previous
7410 * theme doesn't exist, then there will be no close button.
7411 *
7412 * @author Leo Fajardo (@leorw)
7413 * @since 1.2.2
7414 *
7415 * @param string $slug_or_name Old theme's slug or name.
7416 * @param bool|WP_Theme $old_theme WP_Theme instance of the old theme if it still exists.
7417 */
7418 function _activate_theme_event_hook( $slug_or_name, $old_theme = false ) {
7419 $this->_storage->previous_theme = ( false !== $old_theme ) ?
7420 $old_theme->get_stylesheet() :
7421 $slug_or_name;
7422
7423 $this->_activate_plugin_event_hook();
7424 }
7425
7426 /**
7427 * Plugin activated hook.
7428 *
7429 * @author Vova Feldman (@svovaf)
7430 * @since 1.0.1
7431 *
7432 * @uses FS_Api
7433 */
7434 function _activate_plugin_event_hook() {
7435 $this->_logger->entrance( 'slug = ' . $this->_slug );
7436
7437 if ( ! $this->is_user_admin() ) {
7438 return;
7439 }
7440
7441 $this->unregister_uninstall_hook();
7442
7443 // Clear API cache on activation.
7444 FS_Api::clear_cache();
7445
7446 $is_premium_version_activation = $this->is_plugin() ?
7447 ( current_filter() !== ( 'activate_' . $this->_free_plugin_basename ) ) :
7448 $this->is_premium();
7449
7450 $this->_logger->info( 'Activating ' . ( $is_premium_version_activation ? 'premium' : 'free' ) . ' plugin version.' );
7451
7452 if ( $this->is_plugin() ) {
7453 // This logic is relevant only to plugins since both the free and premium versions of a plugin can be active at the same time.
7454 // 1. If running in the activation of the FREE module, get the basename of the PREMIUM.
7455 // 2. If running in the activation of the PREMIUM module, get the basename of the FREE.
7456 $other_version_basename = $is_premium_version_activation ?
7457 $this->_free_plugin_basename :
7458 $this->premium_plugin_basename();
7459
7460 if ( ! $this->_is_network_active ) {
7461 /**
7462 * Themes are always network activated, but the ACTUAL activation is per site.
7463 *
7464 * During the activation, the plugin isn't yet active, therefore,
7465 * _is_network_active will be set to false even if it's a network level
7466 * activation. So we need to fix that by looking at the is_network_admin() value.
7467 *
7468 * @author Vova Feldman
7469 */
7470 $this->_is_network_active = (
7471 $this->_is_multisite_integrated &&
7472 fs_is_network_admin()
7473 );
7474 }
7475
7476 /**
7477 * If the other module version is active, deactivate it.
7478 *
7479 * is_plugin_active() checks if the plugin is active on the site or the network level and
7480 * deactivate_plugins() deactivates the plugin whether it's activated on the site or network level.
7481 *
7482 * @author Leo Fajardo (@leorw)
7483 * @since 1.2.2
7484 */
7485 if ( is_plugin_active( $other_version_basename ) ) {
7486 deactivate_plugins( $other_version_basename );
7487 }
7488 }
7489
7490 if ( $this->is_registered() ) {
7491 if ( $is_premium_version_activation ) {
7492 $this->reconnect_locally();
7493 }
7494
7495
7496 // Schedule re-activation event and sync.
7497 // $this->sync_install( array(), true );
7498 $this->schedule_install_sync();
7499
7500 // If activating the premium module version, add an admin notice to congratulate for an upgrade completion.
7501 if ( $is_premium_version_activation ) {
7502 $this->_admin_notices->add(
7503 sprintf( $this->get_text_inline( 'The upgrade of %s was successfully completed.', 'successful-version-upgrade-message' ), sprintf( '<b>%s</b>', $this->_plugin->title ) ),
7504 $this->get_text_x_inline( 'W00t',
7505 'Used to express elation, enthusiasm, or triumph (especially in electronic communication).', 'woot' ) . '!'
7506 );
7507 }
7508 } else if ( $this->is_anonymous() ) {
7509 if ( isset( $this->_storage->is_anonymous_ms ) && $this->_storage->is_anonymous_ms['is'] ) {
7510 $plugin_version = $this->_storage->is_anonymous_ms['version'];
7511 $network = true;
7512 } else {
7513 $plugin_version = $this->_storage->is_anonymous['version'];
7514 $network = false;
7515 }
7516
7517 /**
7518 * Reset "skipped" click cache on the following:
7519 * 1. Freemius DEV mode.
7520 * 2. WordPress DEBUG mode.
7521 * 3. If a plugin and the user skipped the exact same version before.
7522 *
7523 * @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).
7524 *
7525 * @todo 4. If explicitly asked to retry after every activation.
7526 */
7527 if ( WP_FS__DEV_MODE ||
7528 (
7529 ( $this->is_plugin() || ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ) &&
7530 $this->get_plugin_version() == $plugin_version
7531 )
7532 ) {
7533 $this->reset_anonymous_mode( $network );
7534 }
7535 }
7536
7537 $is_trial_or_has_features_enabled_license = ( $this->is_trial() || $this->has_features_enabled_license() );
7538
7539 if ( $this->is_addon() && ! $is_trial_or_has_features_enabled_license ) {
7540 /**
7541 * When activating an add-on, try to also activate a license.
7542 *
7543 * @author Leo Fajardo (@leorw)
7544 * @since 2.3.0
7545 */
7546 if ( ! $this->_is_network_active ) {
7547 $this->maybe_activate_addon_license();
7548 } else {
7549 $this->maybe_network_activate_addon_license();
7550 }
7551
7552 /**
7553 * Avoid redirecting to the license activation screen after automatically activating an add-on license.
7554 *
7555 * @author Leo Fajardo (@leorw)
7556 * @since 2.3.0
7557 */
7558 $is_trial_or_has_features_enabled_license = ( $this->is_trial() || $this->has_features_enabled_license() );
7559
7560 if ( $is_trial_or_has_features_enabled_license && true === $this->_storage->require_license_activation ) {
7561 $this->_storage->require_license_activation = false;
7562 }
7563 }
7564
7565 if (
7566 $is_premium_version_activation &&
7567 (
7568 ( ! $this->is_registered() && $this->is_anonymous() ) ||
7569 (
7570 $this->is_registered() &&
7571 ! $is_trial_or_has_features_enabled_license
7572 )
7573 )
7574 ) {
7575 $this->_storage->require_license_activation = true;
7576 }
7577
7578 if ( ! isset( $this->_storage->is_plugin_new_install ) ) {
7579 /**
7580 * If no previous version of plugin's version exist, it means that it's either
7581 * the first time that the plugin installed on the site, or the plugin was installed
7582 * before but didn't have Freemius integrated.
7583 *
7584 * Since register_activation_hook() do NOT fires on updates since 3.1, and only fires
7585 * on manual activation via the dashboard, is_plugin_activation() is TRUE
7586 * only after immediate activation.
7587 *
7588 * @since 1.1.4
7589 * @link https://make.wordpress.org/core/2010/10/27/plugin-activation-hooks-no-longer-fire-for-updates/
7590 */
7591 $this->_storage->is_plugin_new_install = empty( $this->_storage->plugin_last_version );
7592 }
7593
7594 if ( ! $this->_anonymous_mode &&
7595 $this->has_api_connectivity( WP_FS__DEV_MODE ) &&
7596 ! $this->_isAutoInstall
7597 ) {
7598 // Store hint that the plugin was just activated to enable auto-redirection to settings.
7599 set_transient( "fs_{$this->_module_type}_{$this->_slug}_activated", true, 60 );
7600 }
7601
7602 /**
7603 * Activation hook is executed after the plugin's main file is loaded, therefore,
7604 * after the plugin was loaded. The logic is located at activate_plugin()
7605 * ./wp-admin/includes/plugin.php.
7606 *
7607 * @author Vova Feldman (@svovaf)
7608 * @since 1.1.9
7609 */
7610 $this->_storage->was_plugin_loaded = true;
7611 }
7612
7613 /**
7614 * @author Leo Fajardo (@leorw)
7615 * @since 2.3.0
7616 */
7617 private function maybe_activate_addon_license() {
7618 $parent_fs = $this->get_parent_instance();
7619
7620 if (
7621 ! is_object( $parent_fs ) ||
7622 ( ! $parent_fs->is_registered() && ! $parent_fs->is_network_registered() )
7623 ) {
7624 // Try to activate a license only if the parent plugin is active and has a valid `install`.
7625 return;
7626 }
7627
7628 $license = $this->get_addon_active_parent_license();
7629 if ( ! is_object( $license ) ) {
7630 return;
7631 }
7632
7633 if ( ! $this->is_registered() ) {
7634 // Opt in with a license key.
7635 $this->opt_in(
7636 $parent_fs->get_current_or_network_user()->email,
7637 false,
7638 false,
7639 $license->secret_key
7640 );
7641 } else {
7642 // Activate the license.
7643 $install = $this->get_api_site_scope()->call(
7644 '/',
7645 'put',
7646 array( 'license_key' => $this->apply_filters( 'license_key', $license->secret_key ) )
7647 );
7648
7649 if ( ! FS_Api::is_api_error( $install ) ) {
7650 $this->_sync_addon_license( $this->get_id(), true );
7651 }
7652 }
7653 }
7654
7655
7656 /**
7657 * @author Leo Fajardo (@leorw)
7658 * @since 2.3.0
7659 *
7660 * @param FS_Plugin_License $license
7661 */
7662 private function maybe_network_activate_addon_license( $license = null ) {
7663 $parent_fs = $this->get_parent_instance();
7664 if ( ! is_object( $parent_fs ) || ( ! $parent_fs->is_registered() && ! $parent_fs->is_network_registered() ) ) {
7665 // Try to activate a license only if the parent plugin is active and has a valid `install`.
7666 return;
7667 }
7668
7669 $license = ( ! is_null( $license ) ) ?
7670 $license :
7671 $this->get_addon_active_parent_license();
7672
7673 if ( ! is_object( $license ) ) {
7674 return;
7675 }
7676
7677 if ( ! $this->is_network_registered() ) {
7678 $sites = $this->get_sites_for_network_level_optin();
7679
7680 if ( count( $sites ) > $license->left() ) {
7681 // If the add-on is network active, try to activate the license only if it can be activated on all sites.
7682 return;
7683 }
7684
7685 // Opt in with a license key.
7686 $this->opt_in(
7687 $parent_fs->get_user()->email,
7688 false,
7689 false,
7690 $license->secret_key,
7691 false,
7692 false,
7693 false,
7694 null,
7695 $sites
7696 );
7697 } else {
7698 $blog_2_install_map = array();
7699 $site_ids = array();
7700
7701 $all_sites = Freemius::get_sites();
7702
7703 foreach ( $all_sites as $site ) {
7704 $blog_id = Freemius::get_site_blog_id( $site );
7705 $install = $this->get_install_by_blog_id( $blog_id );
7706
7707 if ( is_object( $install ) && FS_Plugin_License::is_valid_id( $install->license_id ) ) {
7708 // Skip license activation for installs that are already associated with a license.
7709 continue;
7710 }
7711
7712 if ( is_object( $install ) ) {
7713 $blog_2_install_map[ $blog_id ] = $install;
7714 } else {
7715 $site_ids[] = $blog_id;
7716 }
7717 }
7718
7719 if ( ( count( $blog_2_install_map ) + count( $site_ids ) ) > $license->left() ) {
7720 return;
7721 }
7722
7723 $user = $this->get_current_or_network_user();
7724
7725 if ( ! empty( $blog_2_install_map ) ) {
7726 $result = $this->activate_license_on_many_installs( $user, $license->secret_key, $blog_2_install_map );
7727
7728 if ( true !== $result ) {
7729 return;
7730 }
7731 }
7732
7733 if ( ! empty( $site_ids ) ) {
7734 $this->activate_license_on_many_sites( $user, $license->secret_key, $site_ids );
7735 }
7736 }
7737 }
7738
7739 /**
7740 * @author Leo Fajardo (@leorw)
7741 * @since 2.3.0
7742 *
7743 * @return FS_Plugin_License
7744 */
7745 private function get_addon_active_parent_license() {
7746 $parent_licenses_endpoint = "/plugins/{$this->get_id()}/parent_licenses.json?filter=activatable";
7747 $parent_instance = $this->get_parent_instance();
7748
7749 $foreign_licenses = $parent_instance->get_foreign_licenses_info(
7750 self::get_all_licenses( $this->get_parent_id() )
7751 );
7752
7753 if ( ! empty ( $foreign_licenses ) ) {
7754 $foreign_licenses = array(
7755 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
7756 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
7757 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
7758 );
7759
7760 $parent_licenses_endpoint = add_query_arg( $foreign_licenses, $parent_licenses_endpoint );
7761 }
7762
7763 $result = $parent_instance->get_current_or_network_user_api_scope()->get( $parent_licenses_endpoint, true );
7764
7765 if (
7766 ! $this->is_api_result_object( $result, 'licenses' ) ||
7767 ! is_array( $result->licenses ) ||
7768 empty( $result->licenses )
7769 ) {
7770 return null;
7771 }
7772
7773 $license = new FS_Plugin_License( $result->licenses[ 0 ] );
7774
7775 return $license;
7776 }
7777
7778 /**
7779 * @author Leo Fajardo (@leorw)
7780 * @since 2.3.0
7781 *
7782 * @return array
7783 */
7784 private function get_sites_for_network_level_optin() {
7785 $sites = array();
7786 $all_sites = self::get_sites();
7787
7788 foreach ( $all_sites as $site ) {
7789 $blog_id = self::get_site_blog_id( $site );
7790
7791 if ( ! $this->is_site_delegated_connection( $blog_id ) &&
7792 ! $this->is_installed_on_site( $blog_id )
7793 ) {
7794 $sites[] = $this->get_site_info( $site );
7795 }
7796 }
7797
7798 return $sites;
7799 }
7800
7801 /**
7802 * Delete account.
7803 *
7804 * @author Vova Feldman (@svovaf)
7805 * @since 1.0.3
7806 *
7807 * @param bool $check_user Enforce checking if user have plugins activation privileges.
7808 */
7809 function delete_account_event( $check_user = true ) {
7810 $this->_logger->entrance( 'slug = ' . $this->_slug );
7811
7812 if ( $check_user && ! $this->is_user_admin() ) {
7813 return;
7814 }
7815
7816 $this->do_action( 'before_account_delete' );
7817
7818 // Clear all admin notices.
7819 $this->_admin_notices->clear_all_sticky( false );
7820
7821 $this->_delete_site( false );
7822
7823 $delete_network_common_data = true;
7824
7825 if ( $this->_is_network_active ) {
7826 $installs = $this->get_blog_install_map();
7827
7828 // Don't delete common network data unless no other installs left.
7829 $delete_network_common_data = empty( $installs );
7830 }
7831
7832 if ( $delete_network_common_data ) {
7833 $this->_delete_plans( false );
7834
7835 $this->_delete_licenses( false );
7836
7837 // Delete add-ons related to plugin's account.
7838 $this->_delete_account_addons( false );
7839 }
7840
7841 // @todo Delete plans and licenses of add-ons.
7842
7843 self::$_accounts->store();
7844
7845 /**
7846 * IMPORTANT:
7847 * Clear crons must be executed before clearing all storage.
7848 * Otherwise, the cron will not be cleared.
7849 */
7850 if ( $delete_network_common_data ) {
7851 $this->clear_sync_cron();
7852 }
7853
7854 $this->clear_install_sync_cron();
7855
7856 // Clear all storage data.
7857 $this->_storage->clear_all( true, array(
7858 'is_delegated_connection',
7859 'connectivity_test',
7860 'is_on',
7861 ), false );
7862
7863 // Send delete event.
7864 $this->get_api_site_scope()->call( '/', 'delete' );
7865
7866 $this->do_action( 'after_account_delete' );
7867 }
7868
7869 /**
7870 * Delete network level account.
7871 *
7872 * @author Vova Feldman (@svovaf)
7873 * @since 2.0.0
7874 *
7875 * @param bool $check_user Enforce checking if user have plugins activation privileges.
7876 */
7877 function delete_network_account_event( $check_user = true ) {
7878 $this->_logger->entrance( 'slug = ' . $this->_slug );
7879
7880 if ( $check_user && ! $this->is_user_admin() ) {
7881 return;
7882 }
7883
7884 $this->do_action( 'before_network_account_delete' );
7885
7886 // Clear all admin notices.
7887 $this->_admin_notices->clear_all_sticky();
7888
7889 $this->_delete_plans( false, false );
7890
7891 $this->_delete_licenses( false );
7892
7893 // Delete add-ons related to plugin's account.
7894 $this->_delete_account_addons( false );
7895
7896 // @todo Delete plans and licenses of add-ons.
7897
7898 self::$_accounts->store( true );
7899
7900 /**
7901 * IMPORTANT:
7902 * Clear crons must be executed before clearing all storage.
7903 * Otherwise, the cron will not be cleared.
7904 */
7905 $this->clear_sync_cron( true );
7906 $this->clear_install_sync_cron( true );
7907
7908 $sites = self::get_sites();
7909
7910 $install_ids = array();
7911 foreach ( $sites as $site ) {
7912 $blog_id = self::get_site_blog_id( $site );
7913
7914 $install_id = $this->_delete_site( true, $blog_id );
7915
7916 // Clear all storage data.
7917 $this->_storage->clear_all( true, array( 'connectivity_test' ), $blog_id );
7918
7919 if ( FS_Site::is_valid_id( $install_id ) ) {
7920 $install_ids[] = $install_id;
7921 }
7922
7923 switch_to_blog( $blog_id );
7924
7925 $this->do_action( 'after_account_delete' );
7926
7927 restore_current_blog();
7928 }
7929
7930 $this->_storage->clear_all( true, array(
7931 'connectivity_test',
7932 'is_on',
7933 ), true );
7934
7935 // Send delete event.
7936 if ( ! empty( $install_ids ) ) {
7937 $result = $this->get_current_or_network_user_api_scope()->call( "/plugins/{$this->_module_id}/installs.json?ids=" . implode( ',', $install_ids ), 'delete' );
7938 }
7939
7940 $this->do_action( 'after_network_account_delete' );
7941 }
7942
7943 /**
7944 * Plugin deactivation hook.
7945 *
7946 * @author Vova Feldman (@svovaf)
7947 * @since 1.0.1
7948 */
7949 function _deactivate_plugin_hook() {
7950 $this->_logger->entrance( 'slug = ' . $this->_slug );
7951
7952 if ( ! $this->is_user_admin() ) {
7953 return;
7954 }
7955
7956 $is_network_deactivation = fs_is_network_admin();
7957 $storage_keys_for_removal = array();
7958
7959 $this->_admin_notices->clear_all_sticky();
7960
7961 $storage_keys_for_removal[] = 'sticky_optin_added';
7962 if ( isset( $this->_storage->sticky_optin_added ) ) {
7963 unset( $this->_storage->sticky_optin_added );
7964 }
7965
7966 if ( ! isset( $this->_storage->is_plugin_new_install ) ) {
7967 // Remember that plugin was already installed.
7968 $this->_storage->is_plugin_new_install = false;
7969 }
7970
7971 // Hook to plugin uninstall.
7972 register_uninstall_hook( $this->_plugin_main_file_path, array( 'Freemius', '_uninstall_plugin_hook' ) );
7973
7974 $this->clear_module_main_file_cache();
7975 $this->clear_sync_cron( $this->_is_network_active );
7976 $this->clear_install_sync_cron();
7977
7978 if ( $this->is_registered() ) {
7979 if ( $this->is_premium() && ! $this->has_active_valid_license() ) {
7980 FS_Plugin_Updater::instance( $this )->delete_update_data();
7981 }
7982
7983 if ( $is_network_deactivation ) {
7984 // Send deactivation event.
7985 $this->sync_installs( array(
7986 'is_active' => false,
7987 ) );
7988 } else {
7989 // Send deactivation event.
7990 $this->sync_install( array(
7991 'is_active' => false,
7992 ) );
7993 }
7994 } else {
7995 if ( ! $this->has_api_connectivity() ) {
7996 // Reset connectivity test cache.
7997 unset( $this->_storage->connectivity_test );
7998
7999 $storage_keys_for_removal[] = 'connectivity_test';
8000 }
8001 }
8002
8003 if ( $is_network_deactivation ) {
8004 if ( isset( $this->_storage->sticky_optin_added_ms ) ) {
8005 unset( $this->_storage->sticky_optin_added_ms );
8006 }
8007
8008 if ( ! empty( $storage_keys_for_removal ) ) {
8009 $sites = self::get_sites();
8010
8011 foreach ( $sites as $site ) {
8012 $blog_id = self::get_site_blog_id( $site );
8013
8014 foreach ( $storage_keys_for_removal as $key ) {
8015 $this->_storage->remove( $key, false, $blog_id );
8016 }
8017
8018 $this->_storage->save( $blog_id );
8019 }
8020 }
8021 }
8022
8023 // Clear API cache on deactivation.
8024 FS_Api::clear_cache();
8025
8026 $this->remove_sdk_reference();
8027 }
8028
8029 /**
8030 * @author Vova Feldman (@svovaf)
8031 * @since 1.1.6
8032 */
8033 private function remove_sdk_reference() {
8034 global $fs_active_plugins;
8035
8036 foreach ( $fs_active_plugins->plugins as $sdk_path => $data ) {
8037 if ( $this->_plugin_basename == $data->plugin_path ) {
8038 unset( $fs_active_plugins->plugins[ $sdk_path ] );
8039 break;
8040 }
8041 }
8042
8043 fs_fallback_to_newest_active_sdk();
8044 }
8045
8046 /**
8047 * @author Vova Feldman (@svovaf)
8048 * @since 1.1.3
8049 *
8050 * @param bool $is_anonymous
8051 * @param bool|int $network_or_blog_id Since 2.0.0
8052 */
8053 private function set_anonymous_mode( $is_anonymous = true, $network_or_blog_id = 0 ) {
8054 // Store information regarding skip to try and opt-in the user
8055 // again in the future.
8056 $skip_info = array(
8057 'is' => $is_anonymous,
8058 'timestamp' => WP_FS__SCRIPT_START_TIME,
8059 'version' => $this->get_plugin_version(),
8060 );
8061
8062 if ( true === $network_or_blog_id ) {
8063 $this->_storage->is_anonymous_ms = $skip_info;
8064 } else {
8065 $this->_storage->store( 'is_anonymous', $skip_info, $network_or_blog_id );
8066 }
8067
8068 $this->network_upgrade_mode_completed();
8069
8070 // Update anonymous mode cache.
8071 $this->_is_anonymous = $is_anonymous;
8072 }
8073
8074 /**
8075 * @author Vova Feldman (@svovaf)
8076 * @since 2.0.0
8077 *
8078 * @param int $blog_id Site ID.
8079 * @param int $user_id User ID.
8080 * @param string $domain Site domain.
8081 * @param string $path Site path.
8082 * @param int $network_id Network ID. Only relevant on multi-network installations.
8083 * @param array $meta Metadata. Used to set initial site options.
8084 *
8085 * @uses Freemius::is_license_network_active() to check if the context license was network activated by the super-admin.
8086 * @uses Freemius::is_network_connected() to check if the super-admin network opted-in.
8087 * @uses Freemius::is_network_anonymous() to check if the super-admin network skipped.
8088 * @uses Freemius::is_network_delegated_connection() to check if the super-admin network delegated the connection to the site admins.
8089 */
8090 function _after_new_blog_callback( $blog_id, $user_id, $domain, $path, $network_id, $meta ) {
8091 $this->_logger->entrance();
8092
8093 if ( $this->is_premium() &&
8094 $this->is_network_connected() &&
8095 is_object( $this->_license ) &&
8096 $this->_license->can_activate( FS_Site::is_localhost_by_address( $domain ) ) &&
8097 $this->is_license_network_active( $blog_id )
8098 ) {
8099 /**
8100 * 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.
8101 */
8102 $current_blog_id = get_current_blog_id();
8103 $license = clone $this->_license;
8104
8105 $this->switch_to_blog( $blog_id );
8106
8107 // Opt-in with network user.
8108 $this->install_with_user(
8109 $this->get_network_user(),
8110 $license->secret_key,
8111 false,
8112 false,
8113 false
8114 );
8115
8116 if ( is_object( $this->_site ) ) {
8117 if ( $this->_site->license_id == $license->id ) {
8118 /**
8119 * If the license was activated successfully, sync the license data from the remote server.
8120 */
8121 $this->_license = $license;
8122 $this->sync_site_license();
8123 }
8124 }
8125
8126 $this->switch_to_blog( $current_blog_id );
8127
8128 if ( is_object( $this->_site ) ) {
8129 // Already connected (with or without a license), so no need to continue.
8130 return;
8131 }
8132 }
8133
8134 if ( $this->is_network_anonymous() ) {
8135 /**
8136 * Opt-in was network skipped so automatically skip the opt-in for the new site.
8137 */
8138 $this->skip_site_connection( $blog_id );
8139 } else if ( $this->is_network_delegated_connection() ) {
8140 /**
8141 * Opt-in was network delegated so automatically delegate the opt-in for the new site's admin.
8142 */
8143 $this->delegate_site_connection( $blog_id );
8144 } else if ( $this->is_network_connected() ) {
8145 /**
8146 * Opt-in was network activated so automatically opt-in with the network user and new site admin.
8147 */
8148 $current_blog_id = get_current_blog_id();
8149
8150 $this->switch_to_blog( $blog_id );
8151
8152 // Opt-in with network user.
8153 $this->install_with_user(
8154 $this->get_network_user(),
8155 false,
8156 false,
8157 false,
8158 false
8159 );
8160
8161 $this->switch_to_blog( $current_blog_id );
8162 } else {
8163 /**
8164 * If the super-admin mixed different options (connect, skip, delegated):
8165 * a) If at least one site connection was delegated, then automatically delegate connection.
8166 * 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.
8167 */
8168 $has_delegated_site = false;
8169
8170 $sites = self::get_sites();
8171 foreach ( $sites as $site ) {
8172 $blog_id = self::get_site_blog_id( $site );
8173
8174 if ( $this->is_site_delegated_connection( $blog_id ) ) {
8175 $has_delegated_site = true;
8176 break;
8177 }
8178 }
8179
8180 if ( $has_delegated_site ) {
8181 $this->delegate_site_connection( $blog_id );
8182 } else {
8183 $this->skip_site_connection( $blog_id );
8184 }
8185 }
8186 }
8187
8188 /**
8189 * @author Vova Feldman (@svovaf)
8190 * @since 1.1.3
8191 *
8192 * @param bool|int $network_or_blog_id Since 2.0.0.
8193 */
8194 private function reset_anonymous_mode( $network_or_blog_id = 0 ) {
8195 if ( true === $network_or_blog_id ) {
8196 unset( $this->_storage->is_anonymous_ms );
8197 } else {
8198 $this->_storage->remove( 'is_anonymous', true, $network_or_blog_id );
8199 }
8200
8201 /**
8202 * Ensure that this field is also "false", otherwise, if the current module's type is "theme" and the module
8203 * has no menus, the opt-in popup will not be shown immediately (in this case, the user will have to click
8204 * on the admin notice that contains the opt-in link in order to trigger the opt-in popup).
8205 *
8206 * @author Leo Fajardo (@leorw)
8207 * @since 1.2.2
8208 */
8209 if ( ! $this->_is_network_active ||
8210 0 === $network_or_blog_id ||
8211 get_current_blog_id() == $network_or_blog_id ||
8212 ( true === $network_or_blog_id && fs_is_network_admin() )
8213 ) {
8214 $this->_is_anonymous = null;
8215 }
8216 }
8217
8218 /**
8219 * This is used to ensure that before redirecting to the opt-in page after resetting the anonymous mode or
8220 * deleting the account in the network level, the URL of the page to redirect to is correct.
8221 *
8222 * @author Leo Fajardo (@leorw)
8223 *
8224 * @since 2.1.3
8225 */
8226 private function maybe_set_slug_and_network_menu_exists_flag() {
8227 if ( ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
8228 $this->_menu->set_slug_and_network_menu_exists_flag( $this->_menu->has_menu() ?
8229 $this->_menu->get_slug() :
8230 $this->_slug
8231 );
8232 }
8233 }
8234
8235 /**
8236 * Clears the anonymous mode and redirects to the opt-in screen.
8237 *
8238 * @author Vova Feldman (@svovaf)
8239 * @since 1.1.7
8240 */
8241 function connect_again() {
8242 if ( ! $this->is_anonymous() ) {
8243 return;
8244 }
8245
8246 $this->reset_anonymous_mode( fs_is_network_admin() );
8247
8248 $this->maybe_set_slug_and_network_menu_exists_flag();
8249
8250 fs_redirect( $this->get_activation_url() );
8251 }
8252
8253 /**
8254 * Skip account connect, and set anonymous mode.
8255 *
8256 * @author Vova Feldman (@svovaf)
8257 * @since 1.1.1
8258 *
8259 * @param array|null $sites Since 2.0.0. Specific sites.
8260 * @param bool $skip_all_network Since 2.0.0. If true, skip connection for all sites.
8261 */
8262 function skip_connection( $sites = null, $skip_all_network = false ) {
8263 $this->_logger->entrance();
8264
8265 $this->_admin_notices->remove_sticky( 'connect_account' );
8266
8267 if ( $skip_all_network ) {
8268 $this->set_anonymous_mode( true, true );
8269 }
8270
8271 if ( ! $skip_all_network && empty( $sites ) ) {
8272 $this->skip_site_connection();
8273 } else {
8274 $uids = array();
8275
8276 if ( $skip_all_network ) {
8277 $this->set_anonymous_mode( true, true );
8278
8279 $sites = self::get_sites();
8280 foreach ( $sites as $site ) {
8281 $blog_id = self::get_site_blog_id( $site );
8282 $this->skip_site_connection( $blog_id, false );
8283 $uids[] = $this->get_anonymous_id( $blog_id );
8284 }
8285 } else if ( ! empty( $sites ) ) {
8286 foreach ( $sites as $site ) {
8287 $uids[] = $site['uid'];
8288 $this->skip_site_connection( $site['blog_id'], false );
8289 }
8290 }
8291
8292 // Send anonymous skip event.
8293 // No user identified info nor any tracking will be sent after the user skips the opt-in.
8294 $this->get_api_plugin_scope()->call( 'skip.json', 'put', array(
8295 'uids' => $uids,
8296 ) );
8297 }
8298
8299 $this->network_upgrade_mode_completed();
8300 }
8301
8302 /**
8303 * Skip connection for specific site in the network.
8304 *
8305 * @author Vova Feldman (@svovaf)
8306 * @since 2.0.0
8307 *
8308 * @param int|null $blog_id
8309 * @param bool $send_skip
8310 */
8311 private function skip_site_connection( $blog_id = null, $send_skip = true ) {
8312 $this->_logger->entrance();
8313
8314 $this->_admin_notices->remove_sticky( 'connect_account', $blog_id );
8315
8316 $this->set_anonymous_mode( true, $blog_id );
8317
8318 if ( $send_skip ) {
8319 $this->get_api_plugin_scope()->call( 'skip.json', 'put', array(
8320 'uids' => array( $this->get_anonymous_id( $blog_id ) ),
8321 ) );
8322 }
8323 }
8324
8325 /**
8326 * Plugin version update hook.
8327 *
8328 * @author Vova Feldman (@svovaf)
8329 * @since 1.0.4
8330 */
8331 private function update_plugin_version_event() {
8332 $this->_logger->entrance();
8333
8334 if ( ! $this->is_registered() ) {
8335 return;
8336 }
8337
8338 $this->schedule_install_sync();
8339 // $this->sync_install( array(), true );
8340 }
8341
8342 /**
8343 * Generate an MD5 signature of a plugins collection.
8344 * This helper methods used to identify changes in a plugins collection.
8345 *
8346 * @author Vova Feldman (@svovaf)
8347 * @since 2.0.0
8348 *
8349 * @param array [string]array $plugins
8350 *
8351 * @return string
8352 */
8353 private function get_plugins_thumbprint( $plugins ) {
8354 ksort( $plugins );
8355
8356 $thumbprint = '';
8357 foreach ( $plugins as $basename => $data ) {
8358 $thumbprint .= $data['slug'] . ',' .
8359 $data['Version'] . ',' .
8360 ( $data['is_active'] ? '1' : '0' ) . ';';
8361 }
8362
8363 return md5( $thumbprint );
8364 }
8365
8366 /**
8367 * Return a list of modified plugins since the last sync.
8368 *
8369 * Note:
8370 * There's no point to store a plugins counter since even if the number of
8371 * plugins didn't change, we still need to check if the versions are all the
8372 * same and the activity state is similar.
8373 *
8374 * @author Vova Feldman (@svovaf)
8375 * @since 1.1.8
8376 *
8377 * @return array|false
8378 */
8379 private function get_plugins_data_for_api() {
8380 // Alias.
8381 $site_active_plugins_option_name = 'active_plugins';
8382 $network_plugins_option_name = 'all_plugins';
8383
8384 /**
8385 * Collection of all site level active plugins.
8386 */
8387 $site_active_plugins_cache = self::$_accounts->get_option( $site_active_plugins_option_name );
8388
8389 if ( ! is_object( $site_active_plugins_cache ) ) {
8390 $site_active_plugins_cache = (object) array(
8391 'timestamp' => '',
8392 'md5' => '',
8393 'plugins' => array(),
8394 );
8395 }
8396
8397 $time = time();
8398
8399 if ( ! empty( $site_active_plugins_cache->timestamp ) &&
8400 ( $time - $site_active_plugins_cache->timestamp ) < WP_FS__TIME_5_MIN_IN_SEC
8401 ) {
8402 // Don't send plugin updates if last update was in the past 5 min.
8403 return false;
8404 }
8405
8406 // Write timestamp to lock the logic.
8407 $site_active_plugins_cache->timestamp = $time;
8408 self::$_accounts->set_option( $site_active_plugins_option_name, $site_active_plugins_cache, true );
8409
8410 // Reload options from DB.
8411 self::$_accounts->load( true );
8412 $site_active_plugins_cache = self::$_accounts->get_option( $site_active_plugins_option_name );
8413
8414 if ( $time != $site_active_plugins_cache->timestamp ) {
8415 // If timestamp is different, then another thread captured the lock.
8416 return false;
8417 }
8418
8419 /**
8420 * Collection of all plugins (network level).
8421 */
8422 $network_plugins_cache = self::$_accounts->get_option( $network_plugins_option_name );
8423
8424 if ( ! is_object( $network_plugins_cache ) ) {
8425 $network_plugins_cache = (object) array(
8426 'timestamp' => '',
8427 'md5' => '',
8428 'plugins' => array(),
8429 );
8430 }
8431
8432 // Check if there's a change in plugins.
8433 $network_plugins = self::get_network_plugins();
8434 $site_active_plugins = self::get_site_active_plugins();
8435
8436 $network_plugins_thumbprint = $this->get_plugins_thumbprint( $network_plugins );
8437 $site_active_plugins_thumbprint = $this->get_plugins_thumbprint( $site_active_plugins );
8438
8439 // Check if plugins status changed (version or active/inactive).
8440 $network_plugins_changed = ( $network_plugins_cache->md5 !== $network_plugins_thumbprint );
8441 $site_active_plugins_changed = ( $site_active_plugins_cache->md5 !== $site_active_plugins_thumbprint );
8442
8443 if ( ! $network_plugins_changed &&
8444 ! $site_active_plugins_changed
8445 ) {
8446 // No changes.
8447 return array();
8448 }
8449
8450 $plugins_update_data = array();
8451
8452 foreach ( $network_plugins_cache->plugins as $basename => $data ) {
8453 if ( ! isset( $network_plugins[ $basename ] ) ) {
8454 // Plugin uninstalled.
8455 $uninstalled_plugin_data = $data;
8456 $uninstalled_plugin_data['is_active'] = false;
8457 $uninstalled_plugin_data['is_uninstalled'] = true;
8458 $plugins_update_data[] = $uninstalled_plugin_data;
8459
8460 unset( $network_plugins[ $basename ] );
8461
8462 unset( $network_plugins_cache->plugins[ $basename ] );
8463 unset( $site_active_plugins_cache->plugins[ $basename ] );
8464
8465 continue;
8466 }
8467
8468 $was_active = $data['is_active'] ||
8469 ( isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
8470 true === $site_active_plugins_cache->plugins[ $basename ]['is_active'] );
8471 $is_active = $network_plugins[ $basename ]['is_active'] ||
8472 ( isset( $site_active_plugins[ $basename ] ) &&
8473 $site_active_plugins[ $basename ]['is_active'] );
8474
8475 if ( ! isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
8476 isset( $site_active_plugins[ $basename ] )
8477 ) {
8478 // Plugin was site level activated.
8479 $site_active_plugins_cache->plugins[ $basename ] = $network_plugins[ $basename ];
8480 $site_active_plugins_cache->plugins[ $basename ]['is_active'] = true;
8481 } else if ( isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
8482 ! isset( $site_active_plugins[ $basename ] )
8483 ) {
8484 // Plugin was site level deactivated.
8485 unset( $site_active_plugins_cache->plugins[ $basename ] );
8486 }
8487
8488 $prev_version = $data['version'];
8489 $current_version = $network_plugins[ $basename ]['Version'];
8490
8491 if ( $was_active !== $is_active || $prev_version !== $current_version ) {
8492 // Plugin activated or deactivated, or version changed.
8493
8494 if ( $was_active !== $is_active ) {
8495 if ( $data['is_active'] != $network_plugins[ $basename ]['is_active'] ) {
8496 $network_plugins_cache->plugins[ $basename ]['is_active'] = $data['is_active'];
8497 }
8498 }
8499
8500 if ( $prev_version !== $current_version ) {
8501 $network_plugins_cache->plugins[ $basename ]['Version'] = $current_version;
8502 }
8503
8504 $updated_plugin_data = $data;
8505 $updated_plugin_data['is_active'] = $is_active;
8506 $updated_plugin_data['version'] = $current_version;
8507 $updated_plugin_data['title'] = $network_plugins[ $basename ]['Name'];
8508 $plugins_update_data[] = $updated_plugin_data;
8509 }
8510 }
8511
8512 // Find new plugins that weren't yet seen before.
8513 foreach ( $network_plugins as $basename => $data ) {
8514 if ( ! isset( $network_plugins_cache->plugins[ $basename ] ) ) {
8515 // New plugin.
8516 $new_plugin = array(
8517 'slug' => $data['slug'],
8518 'version' => $data['Version'],
8519 'title' => $data['Name'],
8520 'is_active' => $data['is_active'],
8521 'is_uninstalled' => false,
8522 );
8523
8524 $plugins_update_data[] = $new_plugin;
8525 $network_plugins_cache->plugins[ $basename ] = $new_plugin;
8526
8527 if ( isset( $site_active_plugins[ $basename ] ) ) {
8528 $site_active_plugins_cache->plugins[ $basename ] = $new_plugin;
8529 $site_active_plugins_cache->plugins[ $basename ]['is_active'] = true;
8530 }
8531 }
8532 }
8533
8534 $site_active_plugins_cache->md5 = $site_active_plugins_thumbprint;
8535 $site_active_plugins_cache->timestamp = $time;
8536 self::$_accounts->set_option( $site_active_plugins_option_name, $site_active_plugins_cache, true );
8537
8538 $network_plugins_cache->md5 = $network_plugins_thumbprint;
8539 $network_plugins_cache->timestamp = $time;
8540 self::$_accounts->set_option( $network_plugins_option_name, $network_plugins_cache, true );
8541
8542 return $plugins_update_data;
8543 }
8544
8545 /**
8546 * Return a list of modified themes since the last sync.
8547 *
8548 * Note:
8549 * There's no point to store a themes counter since even if the number of
8550 * themes didn't change, we still need to check if the versions are all the
8551 * same and the activity state is similar.
8552 *
8553 * @author Vova Feldman (@svovaf)
8554 * @since 1.1.8
8555 *
8556 * @return array|false
8557 */
8558 private function get_themes_data_for_api() {
8559 // Alias.
8560 $option_name = 'all_themes';
8561
8562 $all_cached_themes = self::$_accounts->get_option( $option_name );
8563
8564 if ( ! is_object( $all_cached_themes ) ) {
8565 $all_cached_themes = (object) array(
8566 'timestamp' => '',
8567 'md5' => '',
8568 'themes' => array(),
8569 );
8570 }
8571
8572 $time = time();
8573
8574 if ( ! empty( $all_cached_themes->timestamp ) &&
8575 ( $time - $all_cached_themes->timestamp ) < WP_FS__TIME_5_MIN_IN_SEC
8576 ) {
8577 // Don't send theme updates if last update was in the past 5 min.
8578 return false;
8579 }
8580
8581 // Write timestamp to lock the logic.
8582 $all_cached_themes->timestamp = $time;
8583 self::$_accounts->set_option( $option_name, $all_cached_themes, true );
8584
8585 // Reload options from DB.
8586 self::$_accounts->load( true );
8587 $all_cached_themes = self::$_accounts->get_option( $option_name );
8588
8589 if ( $time != $all_cached_themes->timestamp ) {
8590 // If timestamp is different, then another thread captured the lock.
8591 return false;
8592 }
8593
8594 // Get active theme.
8595 $active_theme = wp_get_theme();
8596 $active_theme_stylesheet = $active_theme->get_stylesheet();
8597
8598 // Check if there's a change in themes.
8599 $all_themes = wp_get_themes();
8600
8601 // Check if themes changed.
8602 ksort( $all_themes );
8603
8604 $themes_signature = '';
8605 foreach ( $all_themes as $slug => $data ) {
8606 $is_active = ( $slug === $active_theme_stylesheet );
8607 $themes_signature .= $slug . ',' .
8608 $data->version . ',' .
8609 ( $is_active ? '1' : '0' ) . ';';
8610 }
8611
8612 // Check if themes status changed (version or active/inactive).
8613 $themes_changed = ( $all_cached_themes->md5 !== md5( $themes_signature ) );
8614
8615 $themes_update_data = array();
8616
8617 if ( $themes_changed ) {
8618 // Change in themes, report changes.
8619
8620 // Update existing themes info.
8621 foreach ( $all_cached_themes->themes as $slug => $data ) {
8622 $is_active = ( $slug === $active_theme_stylesheet );
8623
8624 if ( ! isset( $all_themes[ $slug ] ) ) {
8625 // Plugin uninstalled.
8626 $uninstalled_theme_data = $data;
8627 $uninstalled_theme_data['is_active'] = false;
8628 $uninstalled_theme_data['is_uninstalled'] = true;
8629 $themes_update_data[] = $uninstalled_theme_data;
8630
8631 unset( $all_themes[ $slug ] );
8632 unset( $all_cached_themes->themes[ $slug ] );
8633 } else if ( $data['is_active'] !== $is_active ||
8634 $data['version'] !== $all_themes[ $slug ]->version
8635 ) {
8636 // Plugin activated or deactivated, or version changed.
8637
8638 $all_cached_themes->themes[ $slug ]['is_active'] = $is_active;
8639 $all_cached_themes->themes[ $slug ]['version'] = $all_themes[ $slug ]->version;
8640
8641 $themes_update_data[] = $all_cached_themes->themes[ $slug ];
8642 }
8643 }
8644
8645 // Find new themes that weren't yet seen before.
8646 foreach ( $all_themes as $slug => $data ) {
8647 if ( ! isset( $all_cached_themes->themes[ $slug ] ) ) {
8648 $is_active = ( $slug === $active_theme_stylesheet );
8649
8650 // New plugin.
8651 $new_plugin = array(
8652 'slug' => $slug,
8653 'version' => $data->version,
8654 'title' => $data->name,
8655 'is_active' => $is_active,
8656 'is_uninstalled' => false,
8657 );
8658
8659 $themes_update_data[] = $new_plugin;
8660 $all_cached_themes->themes[ $slug ] = $new_plugin;
8661 }
8662 }
8663
8664 $all_cached_themes->md5 = md5( $themes_signature );
8665 $all_cached_themes->timestamp = time();
8666 self::$_accounts->set_option( $option_name, $all_cached_themes, true );
8667 }
8668
8669 return $themes_update_data;
8670 }
8671
8672 /**
8673 * Get site data for API install request.
8674 *
8675 * @author Vova Feldman (@svovaf)
8676 * @since 1.1.2
8677 *
8678 * @param string[] $override
8679 * @param bool $include_plugins Since 1.1.8 by default include plugin changes.
8680 * @param bool $include_themes Since 1.1.8 by default include plugin changes.
8681 * @param bool $include_blog_data Since 2.3.0 by default include the current blog's data (language, charset, title, and URL).
8682 *
8683 * @return array
8684 */
8685 private function get_install_data_for_api(
8686 array $override,
8687 $include_plugins = true,
8688 $include_themes = true,
8689 $include_blog_data = true
8690 ) {
8691 if ( ! defined( 'WP_FS__TRACK_PLUGINS' ) || false !== WP_FS__TRACK_PLUGINS ) {
8692 /**
8693 * @since 1.1.8 Also send plugin updates.
8694 */
8695 if ( $include_plugins && ! isset( $override['plugins'] ) ) {
8696 $plugins = $this->get_plugins_data_for_api();
8697 if ( ! empty( $plugins ) ) {
8698 $override['plugins'] = $plugins;
8699 }
8700 }
8701 }
8702
8703 if ( ! defined( 'WP_FS__TRACK_THEMES' ) || false !== WP_FS__TRACK_THEMES ) {
8704 /**
8705 * @since 1.1.8 Also send themes updates.
8706 */
8707 if ( $include_themes && ! isset( $override['themes'] ) ) {
8708 $themes = $this->get_themes_data_for_api();
8709 if ( ! empty( $themes ) ) {
8710 $override['themes'] = $themes;
8711 }
8712 }
8713 }
8714
8715 $versions = $this->get_versions();
8716
8717 $blog_data = $include_blog_data ?
8718 array(
8719 'language' => get_bloginfo( 'language' ),
8720 'charset' => get_bloginfo( 'charset' ),
8721 'title' => get_bloginfo( 'name' ),
8722 'url' => get_site_url(),
8723 ) :
8724 array();
8725
8726 return array_merge( $versions, $blog_data, array(
8727 'version' => $this->get_plugin_version(),
8728 'is_premium' => $this->is_premium(),
8729 // Special params.
8730 'is_active' => true,
8731 'is_disconnected' => $this->is_tracking_prohibited(),
8732 'is_uninstalled' => false,
8733 ), $override );
8734 }
8735
8736 /**
8737 * Update installs details.
8738 *
8739 * @todo V1 of multiste network support doesn't support plugin and theme data sending.
8740 *
8741 * @author Vova Feldman (@svovaf)
8742 * @since 2.0.0
8743 *
8744 * @param string[] string $override
8745 * @param bool $only_diff
8746 * @param bool $include_plugins Since 1.1.8 by default include plugin changes.
8747 * @param bool $include_themes Since 1.1.8 by default include plugin changes.
8748 *
8749 * @return array
8750 */
8751 private function get_installs_data_for_api(
8752 array $override,
8753 $only_diff = false,
8754 $include_plugins = true,
8755 $include_themes = true
8756 ) {
8757 /**
8758 * @since 1.1.8 Also send plugin updates.
8759 */
8760 // if ( $include_plugins && ! isset( $override['plugins'] ) ) {
8761 // $plugins = $this->get_plugins_data_for_api();
8762 // if ( ! empty( $plugins ) ) {
8763 // $override['plugins'] = $plugins;
8764 // }
8765 // }
8766 /**
8767 * @since 1.1.8 Also send themes updates.
8768 */
8769 // if ( $include_themes && ! isset( $override['themes'] ) ) {
8770 // $themes = $this->get_themes_data_for_api();
8771 // if ( ! empty( $themes ) ) {
8772 // $override['themes'] = $themes;
8773 // }
8774 // }
8775
8776 // Common properties.
8777 $versions = $this->get_versions();
8778 $common = array_merge( $versions, array(
8779 'version' => $this->get_plugin_version(),
8780 'is_premium' => $this->is_premium(),
8781 ), $override );
8782
8783
8784 $is_common_diff_for_any_site = false;
8785 $common_diff_union = array();
8786
8787 $installs_data = array();
8788
8789 $sites = self::get_sites();
8790
8791 foreach ( $sites as $site ) {
8792 $blog_id = self::get_site_blog_id( $site );
8793
8794 $install = $this->get_install_by_blog_id( $blog_id );
8795
8796 if ( is_object( $install ) ) {
8797 if ( $install->user_id != $this->_user->id ) {
8798 // Install belongs to a different owner.
8799 continue;
8800 }
8801
8802 if ( ! $this->is_premium() && $install->is_tracking_prohibited() ) {
8803 // Don't send updates regarding opted-out installs.
8804 continue;
8805 }
8806
8807 $install_data = $this->get_site_info( $site );
8808
8809 $uid = $install_data['uid'];
8810
8811 unset( $install_data['blog_id'] );
8812 unset( $install_data['uid'] );
8813
8814 $install_data['is_disconnected'] = $install->is_disconnected;
8815 $install_data['is_active'] = $this->is_active_for_site( $blog_id );
8816 $install_data['is_uninstalled'] = $install->is_uninstalled;
8817
8818 $common_diff = null;
8819 $is_common_diff = false;
8820 if ( $only_diff ) {
8821 $install_data = $this->get_install_diff_for_api( $install_data, $install, $override );
8822 $common_diff = $this->get_install_diff_for_api( $common, $install, $override );
8823
8824 $is_common_diff = ! empty( $common_diff );
8825
8826 if ( $is_common_diff ) {
8827 foreach ( $common_diff as $k => $v ) {
8828 if ( ! isset( $common_diff_union[ $k ] ) ) {
8829 $common_diff_union[ $k ] = $v;
8830 }
8831 }
8832 }
8833
8834 $is_common_diff_for_any_site = $is_common_diff_for_any_site || $is_common_diff;
8835 }
8836
8837 if ( ! empty( $install_data ) || $is_common_diff ) {
8838 // Add install ID and site unique ID.
8839 $install_data['id'] = $install->id;
8840 $install_data['uid'] = $uid;
8841
8842 $installs_data[] = $install_data;
8843 }
8844 }
8845 }
8846
8847 restore_current_blog();
8848
8849 if ( 0 < count( $installs_data ) && ( $is_common_diff_for_any_site || ! $only_diff ) ) {
8850 if ( ! $only_diff ) {
8851 $installs_data[] = $common;
8852 } else if ( ! empty( $common_diff_union ) ) {
8853 $installs_data[] = $common_diff_union;
8854 }
8855 }
8856
8857 foreach ( $installs_data as &$data ) {
8858 $data = (object) $data;
8859 }
8860
8861 return $installs_data;
8862 }
8863
8864 /**
8865 * Compare site actual data to the stored install data and return the differences for an API data sync.
8866 *
8867 * @author Vova Feldman (@svovaf)
8868 * @since 2.0.0
8869 *
8870 * @param array $site
8871 * @param FS_Site $install
8872 * @param string[] string $override
8873 *
8874 * @return array
8875 */
8876 private function get_install_diff_for_api( $site, $install, $override = array() ) {
8877 $diff = array();
8878 $special = array();
8879 $special_override = false;
8880
8881 foreach ( $site as $p => $v ) {
8882 if ( property_exists( $install, $p ) ) {
8883 if ( ( is_bool( $install->{$p} ) || ! empty( $install->{$p} ) ) &&
8884 $install->{$p} != $v
8885 ) {
8886 $install->{$p} = $v;
8887 $diff[ $p ] = $v;
8888 }
8889 } else {
8890 $special[ $p ] = $v;
8891
8892 if ( isset( $override[ $p ] ) ||
8893 'plugins' === $p ||
8894 'themes' === $p
8895 ) {
8896 $special_override = true;
8897 }
8898 }
8899 }
8900
8901 if ( $special_override || 0 < count( $diff ) ) {
8902 // Add special params only if has at least one
8903 // standard param, or if explicitly requested to
8904 // override a special param or a param which is not exist
8905 // in the install object.
8906 $diff = array_merge( $diff, $special );
8907 }
8908
8909 return $diff;
8910 }
8911
8912 /**
8913 * Update install only if changed.
8914 *
8915 * @author Vova Feldman (@svovaf)
8916 * @since 1.0.9
8917 *
8918 * @param string[] string $override
8919 * @param bool $flush
8920 *
8921 * @return false|object|string
8922 */
8923 private function send_install_update( $override = array(), $flush = false ) {
8924 $this->_logger->entrance();
8925
8926 $check_properties = $this->get_install_data_for_api( $override );
8927
8928 if ( $flush ) {
8929 $params = $check_properties;
8930 } else {
8931 $params = $this->get_install_diff_for_api( $check_properties, $this->_site, $override );
8932 }
8933
8934 $keepalive_only_update = false;
8935 if ( empty( $params ) ) {
8936 $keepalive_only_update = $this->should_send_keepalive_update();
8937
8938 if ( ! $keepalive_only_update ) {
8939 /**
8940 * There are no updates to send including keepalive.
8941 *
8942 * @author Leo Fajardo (@leorw)
8943 * @since 2.2.3
8944 */
8945 return false;
8946 }
8947 }
8948
8949 if ( ! $keepalive_only_update ) {
8950 /**
8951 * Do not update the last install sync timestamp after a keepalive-only call since there were no actual
8952 * updates sent.
8953 *
8954 * @author Leo Fajardo (@leorw)
8955 * @since 2.2.3
8956 */
8957 if ( ! is_multisite() ) {
8958 // Update last install sync timestamp.
8959 $this->set_cron_execution_timestamp( 'install_sync' );
8960 }
8961
8962 $params['uid'] = $this->get_anonymous_id();
8963 }
8964
8965 $this->set_keepalive_timestamp();
8966
8967 // Send updated values to FS.
8968 $site = $this->get_api_site_scope()->call( '/', 'put', $params );
8969
8970 if ( ! $keepalive_only_update && $this->is_api_result_entity( $site ) ) {
8971 /**
8972 * Do not clear scheduled sync after a keepalive-only call since there were no actual updates sent.
8973 *
8974 * @author Leo Fajardo (@leorw)
8975 * @since 2.2.3
8976 */
8977 if ( ! is_multisite() ) {
8978 // I successfully sent install update, clear scheduled sync if exist.
8979 $this->clear_install_sync_cron();
8980 }
8981 }
8982
8983 return $site;
8984 }
8985
8986 /**
8987 * Update installs only if changed.
8988 *
8989 * @author Vova Feldman (@svovaf)
8990 * @since 2.0.0
8991 *
8992 * @param string[] string $override
8993 * @param bool $flush
8994 *
8995 * @return false|object|string
8996 */
8997 private function send_installs_update( $override = array(), $flush = false ) {
8998 $this->_logger->entrance();
8999
9000 $installs_data = $this->get_installs_data_for_api( $override, ! $flush );
9001
9002 $keepalive_only_update = false;
9003 if ( empty( $installs_data ) ) {
9004 /**
9005 * Pass `true` to use the network level storage since the update is for many installs.
9006 *
9007 * @author Leo Fajardo (@leorw)
9008 * @since 2.2.3
9009 */
9010 $keepalive_only_update = $this->should_send_keepalive_update( true );
9011
9012 if ( ! $keepalive_only_update ) {
9013 /**
9014 * There are no updates to send including keepalive.
9015 *
9016 * @author Leo Fajardo (@leorw)
9017 * @since 2.2.3
9018 */
9019 return false;
9020 }
9021 }
9022
9023 if ( ! $keepalive_only_update ) {
9024 // Update last install sync timestamp if there were actual updates sent (i.e., not a keepalive-only call).
9025 $this->set_cron_execution_timestamp( 'install_sync' );
9026 }
9027
9028 /**
9029 * Pass `true` to use the network level storage since the update is for many installs.
9030 *
9031 * @author Leo Fajardo (@leorw)
9032 * @since 2.2.3
9033 */
9034 $this->set_keepalive_timestamp( true );
9035
9036 // Send updated values to FS.
9037 $result = $this->get_api_user_scope()->call( "/plugins/{$this->_plugin->id}/installs.json", 'put', $installs_data );
9038
9039 if ( ! $keepalive_only_update && $this->is_api_result_object( $result, 'installs' ) ) {
9040 // I successfully sent installs update (there was an actual update sent and it's not just a keepalive-only call), clear scheduled sync if exist.
9041 $this->clear_install_sync_cron();
9042 }
9043
9044 return $result;
9045 }
9046
9047 /**
9048 * @author Leo Fajardo (@leorw)
9049 *
9050 * @param bool|null $use_network_level_storage
9051 *
9052 * @return bool
9053 */
9054 private function should_send_keepalive_update( $use_network_level_storage = null ) {
9055 $keepalive_timestamp = $this->_storage->get( 'keepalive_timestamp', 0, $use_network_level_storage );
9056
9057 if ( $keepalive_timestamp < ( time() - WP_FS__TIME_WEEK_IN_SEC ) ) {
9058 // If updated more than 7 days ago, trigger a keepalive and update the time it was triggered.
9059 return true;
9060 } else {
9061 // 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.
9062 return ( 7 == rand( 1, 7 ) );
9063 }
9064 }
9065
9066 /**
9067 * Update install only if changed.
9068 *
9069 * @author Vova Feldman (@svovaf)
9070 * @since 1.0.9
9071 *
9072 * @param string[] string $override
9073 * @param bool $flush
9074 */
9075 private function sync_install( $override = array(), $flush = false ) {
9076 $this->_logger->entrance();
9077
9078 $site = $this->send_install_update( $override, $flush );
9079
9080 if ( false === $site ) {
9081 // No sync required.
9082 return;
9083 }
9084
9085 if ( ! $this->is_api_result_entity( $site ) ) {
9086 // Failed to sync, don't update locally.
9087 return;
9088 }
9089
9090 $this->_site = new FS_Site( $site );
9091
9092 $this->_store_site( true );
9093 }
9094
9095 /**
9096 * Update install only if changed.
9097 *
9098 * @author Vova Feldman (@svovaf)
9099 * @since 1.0.9
9100 *
9101 * @param string[] string $override
9102 * @param bool $flush
9103 */
9104 private function sync_installs( $override = array(), $flush = false ) {
9105 $this->_logger->entrance();
9106
9107 $result = $this->send_installs_update( $override, $flush );
9108
9109 if ( false === $result ) {
9110 // No sync required.
9111 return;
9112 }
9113
9114 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
9115 // Failed to sync, don't update locally.
9116 return;
9117 }
9118
9119 $address_to_blog_map = $this->get_address_to_blog_map();
9120
9121 foreach ( $result->installs as $install ) {
9122 $this->_site = new FS_Site( $install );
9123
9124 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
9125 $blog_id = $address_to_blog_map[ $address ];
9126
9127 $this->_store_site( true, $blog_id );
9128 }
9129 }
9130
9131 /**
9132 * Track install's custom event.
9133 *
9134 * IMPORTANT:
9135 * Custom event tracking is currently only supported for specific clients.
9136 * If you are not one of them, please don't use this method. If you will,
9137 * the API will simply ignore your request based on the plugin ID.
9138 *
9139 * Need custom tracking for your plugin or theme?
9140 * If you are interested in custom event tracking please contact yo@freemius.com
9141 * for further details.
9142 *
9143 * @author Vova Feldman (@svovaf)
9144 * @since 1.2.1
9145 *
9146 * @param string $name Event name.
9147 * @param array $properties Associative key/value array with primitive values only
9148 * @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
9149 * @param bool $once If true, event will be tracked only once. IMPORTANT: Still trigger the API call.
9150 *
9151 * @return object|false Event data or FALSE on failure.
9152 *
9153 * @throws \Freemius_InvalidArgumentException
9154 */
9155 public function track_event( $name, $properties = array(), $process_at = false, $once = false ) {
9156 $this->_logger->entrance( http_build_query( array( 'name' => $name, 'once' => $once ) ) );
9157
9158 if ( ! $this->is_registered() ) {
9159 return false;
9160 }
9161
9162 $event = array( 'type' => $name );
9163
9164 if ( is_numeric( $process_at ) && $process_at > time() ) {
9165 $event['process_at'] = $process_at;
9166 }
9167
9168 if ( $once ) {
9169 $event['once'] = true;
9170 }
9171
9172 if ( ! empty( $properties ) ) {
9173 // Verify associative array values are primitive.
9174 foreach ( $properties as $k => $v ) {
9175 if ( ! is_scalar( $v ) ) {
9176 throw new Freemius_InvalidArgumentException( 'The $properties argument must be an associative key/value array with primitive values only.' );
9177 }
9178 }
9179
9180 $event['properties'] = $properties;
9181 }
9182
9183 $result = $this->get_api_site_scope()->call( 'events.json', 'post', $event );
9184
9185 return $this->is_api_error( $result ) ?
9186 false :
9187 $result;
9188 }
9189
9190 /**
9191 * Track install's custom event only once, but it still triggers the API call.
9192 *
9193 * IMPORTANT:
9194 * Custom event tracking is currently only supported for specific clients.
9195 * If you are not one of them, please don't use this method. If you will,
9196 * the API will simply ignore your request based on the plugin ID.
9197 *
9198 * Need custom tracking for your plugin or theme?
9199 * If you are interested in custom event tracking please contact yo@freemius.com
9200 * for further details.
9201 *
9202 * @author Vova Feldman (@svovaf)
9203 * @since 1.2.1
9204 *
9205 * @param string $name Event name.
9206 * @param array $properties Associative key/value array with primitive values only
9207 * @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
9208 *
9209 * @return object|false Event data or FALSE on failure.
9210 *
9211 * @throws \Freemius_InvalidArgumentException
9212 *
9213 * @user Freemius::track_event()
9214 */
9215 public function track_event_once( $name, $properties = array(), $process_at = false ) {
9216 return $this->track_event( $name, $properties, $process_at, true );
9217 }
9218
9219 /**
9220 * Plugin uninstall hook.
9221 *
9222 * @author Vova Feldman (@svovaf)
9223 * @since 1.0.1
9224 *
9225 * @param bool $check_user Enforce checking if user have plugins activation privileges.
9226 */
9227 function _uninstall_plugin_event( $check_user = true ) {
9228 $this->_logger->entrance( 'slug = ' . $this->_slug );
9229
9230 if ( $check_user && ! current_user_can( 'activate_plugins' ) ) {
9231 return;
9232 }
9233
9234 $params = array();
9235 $uninstall_reason = null;
9236 if ( isset( $this->_storage->uninstall_reason ) ) {
9237 $uninstall_reason = $this->_storage->uninstall_reason;
9238 $params['reason_id'] = $uninstall_reason->id;
9239 $params['reason_info'] = $uninstall_reason->info;
9240 }
9241
9242 if ( ! $this->is_registered() ) {
9243 // Send anonymous uninstall event only if user submitted a feedback.
9244 if ( isset( $uninstall_reason ) ) {
9245 if ( isset( $uninstall_reason->is_anonymous ) && ! $uninstall_reason->is_anonymous ) {
9246 $this->opt_in( false, false, false, false, true );
9247 } else {
9248 $params['uid'] = $this->get_anonymous_id();
9249 $this->get_api_plugin_scope()->call( 'uninstall.json', 'put', $params );
9250 }
9251 }
9252 } else {
9253 $params = array_merge( $params, array(
9254 'is_active' => false,
9255 'is_uninstalled' => true,
9256 ) );
9257
9258 if ( $this->_is_network_active ) {
9259 // Send uninstall event.
9260 $this->send_installs_update( $params );
9261 } else {
9262 // Send uninstall event.
9263 $this->send_install_update( $params );
9264 }
9265 }
9266
9267 // @todo Decide if we want to delete plugin information from db.
9268 }
9269
9270 /**
9271 * Set the basename of the current product and hook _activate_plugin_event_hook() to the activation action.
9272 *
9273 * @author Vova Feldman (@svovaf)
9274 * @since 2.2.1
9275 *
9276 * @param string $is_premium
9277 * @param string $caller
9278 *
9279 * @return string
9280 */
9281 function set_basename( $is_premium, $caller ) {
9282 $basename = plugin_basename( $caller );
9283
9284 $current_basename = $is_premium ?
9285 $this->_premium_plugin_basename :
9286 $this->_free_plugin_basename;
9287
9288 if ( $current_basename == $basename ) {
9289 // Basename value set correctly.
9290 return;
9291 }
9292
9293 if ( $is_premium ) {
9294 $this->_premium_plugin_basename = $basename;
9295 } else {
9296 $this->_free_plugin_basename = $basename;
9297 }
9298
9299 $plugin_dir = dirname( $this->_plugin_dir_path ) . '/';
9300
9301 register_activation_hook(
9302 $plugin_dir . $basename,
9303 array( &$this, '_activate_plugin_event_hook' )
9304 );
9305 }
9306
9307 /**
9308 * @author Vova Feldman (@svovaf)
9309 * @since 1.1.1
9310 * @since 2.2.1 If the context product is in its premium version, use the current module's basename, even if it was renamed.
9311 *
9312 * @return string
9313 */
9314 function premium_plugin_basename() {
9315 if ( ! isset( $this->_premium_plugin_basename ) ) {
9316 $this->_premium_plugin_basename = $this->is_premium() ?
9317 // The product is premium, so use the current basename.
9318 $this->_plugin_basename :
9319 $this->get_premium_slug() . '/' . basename( $this->_free_plugin_basename );
9320 }
9321
9322 return $this->_premium_plugin_basename;
9323 }
9324
9325 /**
9326 * Uninstall plugin hook. Called only when connected his account with Freemius for active sites tracking.
9327 *
9328 * @author Vova Feldman (@svovaf)
9329 * @since 1.0.2
9330 */
9331 public static function _uninstall_plugin_hook() {
9332 self::_load_required_static();
9333
9334 self::$_static_logger->entrance();
9335
9336 if ( ! current_user_can( 'activate_plugins' ) ) {
9337 return;
9338 }
9339
9340 $plugin_file = substr( current_filter(), strlen( 'uninstall_' ) );
9341
9342 self::$_static_logger->info( 'plugin = ' . $plugin_file );
9343
9344 define( 'WP_FS__UNINSTALL_MODE', true );
9345
9346 $fs = self::get_instance_by_file( $plugin_file );
9347
9348 if ( is_object( $fs ) ) {
9349 self::require_plugin_essentials();
9350
9351 if ( is_plugin_active( $fs->_free_plugin_basename ) ||
9352 is_plugin_active( $fs->premium_plugin_basename() )
9353 ) {
9354 // Deleting Free or Premium plugin version while the other version still installed.
9355 return;
9356 }
9357
9358 $fs->_uninstall_plugin_event();
9359
9360 $fs->do_action( 'after_uninstall' );
9361 }
9362 }
9363
9364 #----------------------------------------------------------------------------------
9365 #region Plugin Information
9366 #----------------------------------------------------------------------------------
9367
9368 /**
9369 * Load WordPress core plugin.php essential module.
9370 *
9371 * @author Vova Feldman (@svovaf)
9372 * @since 1.1.1
9373 */
9374 private static function require_plugin_essentials() {
9375 if ( ! function_exists( 'get_plugins' ) ) {
9376 self::$_static_logger->log( 'Including wp-admin/includes/plugin.php...' );
9377
9378 require_once ABSPATH . 'wp-admin/includes/plugin.php';
9379 }
9380 }
9381
9382 /**
9383 * Load WordPress core pluggable.php module.
9384 *
9385 * @author Vova Feldman (@svovaf)
9386 * @since 1.1.2
9387 */
9388 private static function require_pluggable_essentials() {
9389 if ( ! function_exists( 'wp_get_current_user' ) ) {
9390 require_once ABSPATH . 'wp-includes/pluggable.php';
9391 }
9392 }
9393
9394 /**
9395 * Return plugin data.
9396 *
9397 * @author Vova Feldman (@svovaf)
9398 * @since 1.0.1
9399 *
9400 * @return array
9401 */
9402 function get_plugin_data() {
9403 if ( ! isset( $this->_plugin_data ) ) {
9404 self::require_plugin_essentials();
9405
9406 if ( $this->is_plugin() ) {
9407 /**
9408 * @author Vova Feldman (@svovaf)
9409 * @since 1.2.0 When using get_plugin_data() do NOT translate plugin data.
9410 *
9411 * @link https://github.com/Freemius/wordpress-sdk/issues/77
9412 */
9413 $plugin_data = get_plugin_data(
9414 $this->_plugin_main_file_path,
9415 false,
9416 false
9417 );
9418 } else {
9419 $theme_data = wp_get_theme();
9420
9421 if ( $this->_plugin_basename !== $theme_data->get_stylesheet() && is_child_theme() ) {
9422 $parent_theme = $theme_data->parent();
9423
9424 if ( ( $parent_theme instanceof WP_Theme ) && $this->_plugin_basename === $parent_theme->get_stylesheet() ) {
9425 $theme_data = $parent_theme;
9426 }
9427 }
9428
9429 $plugin_data = array(
9430 'Name' => $theme_data->get( 'Name' ),
9431 'Version' => $theme_data->get( 'Version' ),
9432 'Author' => $theme_data->get( 'Author' ),
9433 'Description' => $theme_data->get( 'Description' ),
9434 'PluginURI' => $theme_data->get( 'ThemeURI' ),
9435 );
9436 }
9437
9438 $this->_plugin_data = $plugin_data;
9439 }
9440
9441 return $this->_plugin_data;
9442 }
9443
9444 /**
9445 * @author Vova Feldman (@svovaf)
9446 * @since 1.0.1
9447 * @since 1.2.2.5 If slug not set load slug by module ID.
9448 *
9449 * @return string Plugin slug.
9450 */
9451 function get_slug() {
9452 if ( ! isset( $this->_slug ) ) {
9453 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
9454 $this->_slug = $id_slug_type_path_map[ $this->_module_id ]['slug'];
9455 }
9456
9457 return $this->_slug;
9458 }
9459
9460 /**
9461 * @author Leo Fajardo (@leorw)
9462 * @since 2.2.1
9463 *
9464 * @return string
9465 */
9466 function get_premium_slug() {
9467 return is_object( $this->_plugin ) ?
9468 $this->_plugin->premium_slug :
9469 "{$this->_slug}-premium";
9470 }
9471
9472 /**
9473 * Retrieve the desired folder name for the product.
9474 *
9475 * @author Vova Feldman (@svovaf)
9476 * @since 1.2.1.7
9477 *
9478 * @return string Plugin slug.
9479 */
9480 function get_target_folder_name() {
9481 return $this->can_use_premium_code() ?
9482 $this->_plugin->premium_slug :
9483 $this->_slug;
9484 }
9485
9486 /**
9487 * @author Vova Feldman (@svovaf)
9488 * @since 1.0.1
9489 *
9490 * @return number Plugin ID.
9491 */
9492 function get_id() {
9493 return $this->_plugin->id;
9494 }
9495
9496 /**
9497 * @author Leo Fajardo (@leorw)
9498 * @since 2.2.4
9499 *
9500 * @return number|null Bundle ID.
9501 */
9502 function get_bundle_id() {
9503 return ( isset( $this->_plugin->bundle_id ) && FS_Plugin::is_valid_id( $this->_plugin->bundle_id ) ) ?
9504 $this->_plugin->bundle_id :
9505 null;
9506 }
9507
9508 /**
9509 * @author Vova Feldman (@svovaf)
9510 * @since 1.2.1.5
9511 *
9512 * @return string Freemius SDK version
9513 */
9514 function get_sdk_version() {
9515 return $this->version;
9516 }
9517
9518 /**
9519 * @author Vova Feldman (@svovaf)
9520 * @since 1.2.1.5
9521 *
9522 * @return number Parent plugin ID (if parent exist).
9523 */
9524 function get_parent_id() {
9525 return $this->is_addon() ?
9526 $this->get_parent_instance()->get_id() :
9527 $this->_plugin->id;
9528 }
9529
9530 /**
9531 * @author Vova Feldman (@svovaf)
9532 * @since 1.0.1
9533 *
9534 * @return string Plugin public key.
9535 */
9536 function get_public_key() {
9537 return $this->_plugin->public_key;
9538 }
9539
9540 /**
9541 * Will be available only on sandbox mode.
9542 *
9543 * @author Vova Feldman (@svovaf)
9544 * @since 1.0.4
9545 *
9546 * @return mixed Plugin secret key.
9547 */
9548 function get_secret_key() {
9549 return $this->_plugin->secret_key;
9550 }
9551
9552 /**
9553 * @author Vova Feldman (@svovaf)
9554 * @since 1.1.1
9555 *
9556 * @return bool
9557 */
9558 function has_secret_key() {
9559 return ! empty( $this->_plugin->secret_key );
9560 }
9561
9562 /**
9563 * @author Vova Feldman (@svovaf)
9564 * @since 1.0.9
9565 *
9566 * @param string|bool $premium_suffix
9567 *
9568 * @return string
9569 */
9570 function get_plugin_name( $premium_suffix = false ) {
9571 $this->_logger->entrance();
9572
9573 /**
9574 * This `if-else` can be squeezed into a single `if` but I intentionally split it for code readability.
9575 *
9576 * @author Vova Feldman
9577 */
9578 if ( ! isset( $this->_plugin_name ) ) {
9579 // Name is not yet set.
9580 $this->set_name( $premium_suffix );
9581 } else if (
9582 ! empty( $premium_suffix ) &&
9583 ( ! is_object( $this->_plugin ) || $this->_plugin->premium_suffix !== $premium_suffix )
9584 ) {
9585 // Name is already set, but there's a change in the premium suffix.
9586 $this->set_name( $premium_suffix );
9587 }
9588
9589 return $this->_plugin_name;
9590 }
9591
9592 /**
9593 * Calculates and stores the product's name. This helper function was created specifically for get_plugin_name() just to make the code clearer.
9594 *
9595 * @author Vova Feldman (@svovaf)
9596 * @since 2.2.1
9597 *
9598 * @param string $premium_suffix
9599 */
9600 private function set_name( $premium_suffix = '' ) {
9601 $plugin_data = $this->get_plugin_data();
9602
9603 // Get name.
9604 $this->_plugin_name = $plugin_data['Name'];
9605
9606 if ( is_string( $premium_suffix ) ) {
9607 $premium_suffix = trim( $premium_suffix );
9608
9609 if ( ! empty( $premium_suffix ) ) {
9610 // Check if plugin name contains " (premium)" or a custom suffix and remove it.
9611 $suffix = ( ' ' . strtolower( $premium_suffix ) );
9612 $suffix_len = strlen( $suffix );
9613
9614 if ( strlen( $plugin_data['Name'] ) > $suffix_len &&
9615 $suffix === substr( strtolower( $plugin_data['Name'] ), - $suffix_len )
9616 ) {
9617 $this->_plugin_name = substr( $plugin_data['Name'], 0, - $suffix_len );
9618 }
9619 }
9620 }
9621
9622 $this->_logger->departure( 'Name = ' . $this->_plugin_name );
9623 }
9624
9625 /**
9626 * @author Vova Feldman (@svovaf)
9627 * @since 1.0.0
9628 *
9629 * @return string
9630 */
9631 function get_plugin_version() {
9632 $this->_logger->entrance();
9633
9634 $plugin_data = $this->get_plugin_data();
9635
9636 $this->_logger->departure( 'Version = ' . $plugin_data['Version'] );
9637
9638 return $this->apply_filters( 'plugin_version', $plugin_data['Version'] );
9639 }
9640
9641 /**
9642 * @author Vova Feldman (@svovaf)
9643 * @since 1.2.1.7
9644 *
9645 * @return string
9646 */
9647 function get_plugin_title() {
9648 $this->_logger->entrance();
9649
9650 $title = $this->_plugin->title;
9651
9652 return $this->apply_filters( 'plugin_title', $title );
9653 }
9654
9655 /**
9656 * @author Vova Feldman (@svovaf)
9657 * @since 1.2.2.7
9658 *
9659 * @param bool $lowercase
9660 *
9661 * @return string
9662 */
9663 function get_module_label( $lowercase = false ) {
9664 $label = $this->is_addon() ?
9665 $this->get_text_inline( 'Add-On', 'addon' ) :
9666 ( $this->is_plugin() ?
9667 $this->get_text_inline( 'Plugin', 'plugin' ) :
9668 $this->get_text_inline( 'Theme', 'theme' ) );
9669
9670 if ( $lowercase ) {
9671 $label = strtolower( $label );
9672 }
9673
9674 return $label;
9675 }
9676
9677 /**
9678 * @author Vova Feldman (@svovaf)
9679 * @since 1.0.4
9680 *
9681 * @return string
9682 */
9683 function get_plugin_basename() {
9684 if ( ! isset( $this->_plugin_basename ) ) {
9685 if ( $this->is_plugin() ) {
9686 $this->_plugin_basename = plugin_basename( $this->_plugin_main_file_path );
9687 } else {
9688 $this->_plugin_basename = basename( dirname( $this->_plugin_main_file_path ) );
9689 }
9690 }
9691
9692 return $this->_plugin_basename;
9693 }
9694
9695 function get_plugin_folder_name() {
9696 $this->_logger->entrance();
9697
9698 $plugin_folder = $this->_plugin_basename;
9699
9700 while ( '.' !== dirname( $plugin_folder ) ) {
9701 $plugin_folder = dirname( $plugin_folder );
9702 }
9703
9704 $this->_logger->departure( 'Folder Name = ' . $plugin_folder );
9705
9706 return $plugin_folder;
9707 }
9708
9709 #endregion ------------------------------------------------------------------
9710
9711 /* Account
9712 ------------------------------------------------------------------------------------------------------------------*/
9713
9714 /**
9715 * Find plugin's slug by plugin's basename.
9716 *
9717 * @author Vova Feldman (@svovaf)
9718 * @since 1.0.9
9719 *
9720 * @param string $plugin_base_name
9721 *
9722 * @return false|string
9723 */
9724 private static function find_slug_by_basename( $plugin_base_name ) {
9725 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
9726
9727 if ( ! array( $file_slug_map ) || ! isset( $file_slug_map[ $plugin_base_name ] ) ) {
9728 return false;
9729 }
9730
9731 return $file_slug_map[ $plugin_base_name ];
9732 }
9733
9734 /**
9735 * Store the map between the plugin's basename to the slug.
9736 *
9737 * @author Vova Feldman (@svovaf)
9738 * @since 1.0.9
9739 */
9740 private function store_file_slug_map() {
9741 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
9742
9743 if ( ! array( $file_slug_map ) ) {
9744 $file_slug_map = array();
9745 }
9746
9747 if ( ! isset( $file_slug_map[ $this->_plugin_basename ] ) ||
9748 $file_slug_map[ $this->_plugin_basename ] !== $this->_slug
9749 ) {
9750 $file_slug_map[ $this->_plugin_basename ] = $this->_slug;
9751 self::$_accounts->set_option( 'file_slug_map', $file_slug_map, true );
9752 }
9753 }
9754
9755 /**
9756 * @return array[number]FS_User
9757 */
9758 static function get_all_users() {
9759 $users = self::$_accounts->get_option( 'users', array() );
9760
9761 if ( ! is_array( $users ) ) {
9762 $users = array();
9763 }
9764
9765 return $users;
9766 }
9767
9768 /**
9769 * @param string $module_type
9770 * @param null|int $blog_id Since 2.0.0
9771 *
9772 * @return array[string]FS_Site
9773 */
9774 private static function get_all_sites(
9775 $module_type = WP_FS__MODULE_TYPE_PLUGIN,
9776 $blog_id = null
9777 ) {
9778 $sites = self::get_account_option( 'sites', $module_type, $blog_id );
9779
9780 if ( ! is_array( $sites ) ) {
9781 $sites = array();
9782 }
9783
9784 return $sites;
9785 }
9786
9787 /**
9788 * @author Leo Fajardo (@leorw)
9789 *
9790 * @since 1.2.2
9791 *
9792 * @param string $option_name
9793 * @param string $module_type
9794 * @param null|int $network_level_or_blog_id Since 2.0.0
9795 *
9796 * @return mixed
9797 */
9798 private static function get_account_option( $option_name, $module_type = null, $network_level_or_blog_id = null ) {
9799 if ( ! is_null( $module_type ) && WP_FS__MODULE_TYPE_PLUGIN !== $module_type ) {
9800 $option_name = $module_type . '_' . $option_name;
9801 }
9802
9803 return self::$_accounts->get_option( $option_name, array(), $network_level_or_blog_id );
9804 }
9805
9806 /**
9807 * @author Leo Fajardo (@leorw)
9808 *
9809 * @since 1.2.2
9810 *
9811 * @param string $option_name
9812 * @param mixed $option_value
9813 * @param bool $store
9814 * @param null|int $network_level_or_blog_id Since 2.0.0
9815 */
9816 private function set_account_option( $option_name, $option_value, $store, $network_level_or_blog_id = null ) {
9817 self::set_account_option_by_module(
9818 $this->_module_type,
9819 $option_name,
9820 $option_value,
9821 $store,
9822 $network_level_or_blog_id
9823 );
9824 }
9825
9826 /**
9827 * @author Vova Feldman (@svovaf)
9828 *
9829 * @since 1.2.2.7
9830 *
9831 * @param string $module_type
9832 * @param string $option_name
9833 * @param mixed $option_value
9834 * @param bool $store
9835 * @param null|int $network_level_or_blog_id Since 2.0.0
9836 */
9837 private static function set_account_option_by_module(
9838 $module_type,
9839 $option_name,
9840 $option_value,
9841 $store,
9842 $network_level_or_blog_id = null
9843 ) {
9844 if ( WP_FS__MODULE_TYPE_PLUGIN != $module_type ) {
9845 $option_name = $module_type . '_' . $option_name;
9846 }
9847
9848 self::$_accounts->set_option( $option_name, $option_value, $store, $network_level_or_blog_id );
9849 }
9850
9851 /**
9852 * @author Vova Feldman (@svovaf)
9853 * @since 1.0.6
9854 *
9855 * @param number|null $module_id
9856 *
9857 * @return FS_Plugin_License[]
9858 */
9859 private static function get_all_licenses( $module_id = null ) {
9860 $licenses = self::get_account_option( 'all_licenses' );
9861
9862 if ( ! is_array( $licenses ) ) {
9863 $licenses = array();
9864 }
9865
9866 if ( is_null( $module_id ) ) {
9867 return $licenses;
9868 }
9869
9870 $licenses = isset( $licenses[ $module_id ] ) ?
9871 $licenses[ $module_id ] :
9872 array();
9873
9874 return $licenses;
9875 }
9876
9877 /**
9878 * @author Leo Fajardo (@leorw)
9879 * @since 2.0.0
9880 *
9881 * @return array
9882 */
9883 private static function get_all_licenses_by_module_type() {
9884 $licenses = self::get_account_option( 'all_licenses' );
9885
9886 $licenses_by_module_type = array(
9887 WP_FS__MODULE_TYPE_PLUGIN => array(),
9888 WP_FS__MODULE_TYPE_THEME => array()
9889 );
9890
9891 if ( ! is_array( $licenses ) ) {
9892 return $licenses_by_module_type;
9893 }
9894
9895 foreach ( $licenses as $module_id => $module_licenses ) {
9896 $fs = self::get_instance_by_id( $module_id );
9897 if ( false === $fs ) {
9898 continue;
9899 }
9900
9901 $licenses_by_module_type[ $fs->_module_type ] = array_merge( $licenses_by_module_type[ $fs->_module_type ], $module_licenses );
9902 }
9903
9904 return $licenses_by_module_type;
9905 }
9906
9907 /**
9908 * @author Leo Fajardo (@leorw)
9909 * @since 2.0.0
9910 *
9911 * @param number $module_id
9912 * @param number|null $user_id
9913 *
9914 * @return array
9915 */
9916 private static function get_user_id_license_ids_map( $module_id, $user_id = null ) {
9917 $all_modules_user_id_license_ids_map = self::get_account_option( 'user_id_license_ids_map' );
9918
9919 if ( ! is_array( $all_modules_user_id_license_ids_map ) ) {
9920 $all_modules_user_id_license_ids_map = array();
9921 }
9922
9923 $user_id_license_ids_map = isset( $all_modules_user_id_license_ids_map[ $module_id ] ) ?
9924 $all_modules_user_id_license_ids_map[ $module_id ] :
9925 array();
9926
9927 if ( FS_User::is_valid_id( $user_id ) ) {
9928 $user_id_license_ids_map = isset( $user_id_license_ids_map[ $user_id ] ) ?
9929 $user_id_license_ids_map[ $user_id ] :
9930 array();
9931 }
9932
9933 return $user_id_license_ids_map;
9934 }
9935
9936 /**
9937 * @author Leo Fajardo (@leorw)
9938 * @since 2.0.0
9939 *
9940 * @param array $new_user_id_license_ids_map
9941 * @param number $module_id
9942 * @param number|null $user_id
9943 */
9944 private static function store_user_id_license_ids_map( $new_user_id_license_ids_map, $module_id, $user_id = null ) {
9945 $all_modules_user_id_license_ids_map = self::get_account_option( 'user_id_license_ids_map' );
9946 if ( ! is_array( $all_modules_user_id_license_ids_map ) ) {
9947 $all_modules_user_id_license_ids_map = array();
9948 }
9949
9950 if ( ! isset( $all_modules_user_id_license_ids_map[ $module_id ] ) ) {
9951 $all_modules_user_id_license_ids_map[ $module_id ] = array();
9952 }
9953
9954 if ( FS_User::is_valid_id( $user_id ) ) {
9955 $all_modules_user_id_license_ids_map[ $module_id ][ $user_id ] = $new_user_id_license_ids_map;
9956 } else {
9957 $all_modules_user_id_license_ids_map[ $module_id ] = $new_user_id_license_ids_map;
9958 }
9959
9960 self::$_accounts->set_option( 'user_id_license_ids_map', $all_modules_user_id_license_ids_map, true );
9961 }
9962
9963 /**
9964 * Get a collection of the user's linked license IDs.
9965 *
9966 * @author Vova Feldman (@svovaf)
9967 * @since 2.0.0
9968 *
9969 * @param number $user_id
9970 *
9971 * @return number[]
9972 */
9973 private function get_user_linked_license_ids( $user_id ) {
9974 return self::get_user_id_license_ids_map( $this->_module_id, $user_id );
9975 }
9976
9977 /**
9978 * Override the user's linked license IDs with a new IDs collection.
9979 *
9980 * @author Vova Feldman (@svovaf)
9981 * @since 2.0.0
9982 *
9983 * @param number $user_id
9984 * @param number[] $license_ids
9985 */
9986 private function set_user_linked_license_ids( $user_id, array $license_ids ) {
9987 self::store_user_id_license_ids_map( $license_ids, $this->_module_id, $user_id );
9988 }
9989
9990 /**
9991 * Link a specified license ID to a given user.
9992 *
9993 * @author Vova Feldman (@svovaf)
9994 * @since 2.0.0
9995 *
9996 * @param number $license_id
9997 * @param number $user_id
9998 */
9999 private function link_license_2_user( $license_id, $user_id ) {
10000 $license_ids = $this->get_user_linked_license_ids( $user_id );
10001
10002 if ( in_array( $license_id, $license_ids ) ) {
10003 // License already linked.
10004 return;
10005 }
10006
10007 $license_ids[] = $license_id;
10008
10009 $this->set_user_linked_license_ids( $user_id, $license_ids );
10010 }
10011
10012 /**
10013 * @param string|bool $module_type
10014 *
10015 * @return FS_Plugin_Plan[]
10016 */
10017 private static function get_all_plans( $module_type = false ) {
10018 $plans = self::get_account_option( 'plans', $module_type );
10019
10020 if ( ! is_array( $plans ) ) {
10021 $plans = array();
10022 }
10023
10024 return $plans;
10025 }
10026
10027 /**
10028 * @author Vova Feldman (@svovaf)
10029 * @since 1.0.4
10030 *
10031 * @return FS_Plugin_Tag[]
10032 */
10033 private static function get_all_updates() {
10034 $updates = self::$_accounts->get_option( 'updates', array() );
10035
10036 if ( ! is_array( $updates ) ) {
10037 $updates = array();
10038 }
10039
10040 return $updates;
10041 }
10042
10043 /**
10044 * @author Vova Feldman (@svovaf)
10045 * @since 1.0.6
10046 *
10047 * @return array<number,FS_Plugin[]>|false
10048 */
10049 private static function get_all_addons() {
10050 $addons = self::$_accounts->get_option( 'addons', array() );
10051
10052 if ( ! is_array( $addons ) ) {
10053 $addons = array();
10054 }
10055
10056 return $addons;
10057 }
10058
10059 /**
10060 * @author Vova Feldman (@svovaf)
10061 * @since 1.0.6
10062 *
10063 * @return FS_Plugin[]|false
10064 */
10065 private static function get_all_account_addons() {
10066 $addons = self::$_accounts->get_option( 'account_addons', array() );
10067
10068 if ( ! is_array( $addons ) ) {
10069 $addons = array();
10070 }
10071
10072 return $addons;
10073 }
10074
10075 /**
10076 * Check if user has connected his account (opted-in).
10077 *
10078 * Note:
10079 * If the user opted-in and opted-out on a later stage,
10080 * this will still return true. If you want to check if the
10081 * user is currently opted-in, use:
10082 * `$fs->is_registered() && $fs->is_tracking_allowed()`
10083 *
10084 * @author Vova Feldman (@svovaf)
10085 * @since 1.0.1
10086 * @return bool
10087 */
10088 function is_registered() {
10089 return is_object( $this->_user );
10090 }
10091
10092 /**
10093 * Returns TRUE if the user opted-in and didn't disconnect (opt-out).
10094 *
10095 * @author Leo Fajardo (@leorw)
10096 * @since 1.2.1.5
10097 *
10098 * @return bool
10099 */
10100 function is_tracking_allowed() {
10101 return ( is_object( $this->_site ) && $this->_site->is_tracking_allowed() );
10102 }
10103
10104 /**
10105 * @author Vova Feldman (@svovaf)
10106 * @since 1.0.4
10107 *
10108 * @return FS_Plugin
10109 */
10110 function get_plugin() {
10111 return $this->_plugin;
10112 }
10113
10114 /**
10115 * @author Vova Feldman (@svovaf)
10116 * @since 1.0.3
10117 *
10118 * @return FS_User
10119 */
10120 function get_user() {
10121 return $this->_user;
10122 }
10123
10124 /**
10125 * @author Vova Feldman (@svovaf)
10126 * @since 1.0.3
10127 *
10128 * @return FS_Site
10129 */
10130 function get_site() {
10131 return $this->_site;
10132 }
10133
10134 /**
10135 * Get plugin add-ons.
10136 *
10137 * @author Vova Feldman (@svovaf)
10138 * @since 1.0.6
10139 *
10140 * @since 1.1.7.3 If not yet loaded, fetch data from the API.
10141 *
10142 * @param bool $flush
10143 *
10144 * @return FS_Plugin[]|false
10145 */
10146 function get_addons( $flush = false ) {
10147 $this->_logger->entrance();
10148
10149 if ( ! $this->_has_addons ) {
10150 return false;
10151 }
10152
10153 $addons = $this->sync_addons( $flush );
10154
10155 return ( ! is_array( $addons ) || empty( $addons ) ) ?
10156 false :
10157 $addons;
10158 }
10159
10160 /**
10161 * @author Vova Feldman (@svovaf)
10162 * @since 1.0.6
10163 *
10164 * @return FS_Plugin[]|false
10165 */
10166 function get_account_addons() {
10167 $this->_logger->entrance();
10168
10169 $addons = self::get_all_account_addons();
10170
10171 if ( ! is_array( $addons ) ||
10172 ! isset( $addons[ $this->_plugin->id ] ) ||
10173 ! is_array( $addons[ $this->_plugin->id ] ) ||
10174 0 === count( $addons[ $this->_plugin->id ] )
10175 ) {
10176 return false;
10177 }
10178
10179 return $addons[ $this->_plugin->id ];
10180 }
10181
10182 /**
10183 * Check if user has any
10184 *
10185 * @author Vova Feldman (@svovaf)
10186 * @since 1.1.6
10187 *
10188 * @return bool
10189 */
10190 function has_account_addons() {
10191 $addons = $this->get_account_addons();
10192
10193 return is_array( $addons ) && ( 0 < count( $addons ) );
10194 }
10195
10196
10197 /**
10198 * Get add-on by ID (from local data).
10199 *
10200 * @author Vova Feldman (@svovaf)
10201 * @since 1.0.6
10202 *
10203 * @param number $id
10204 *
10205 * @return FS_Plugin|false
10206 */
10207 function get_addon( $id ) {
10208 $this->_logger->entrance();
10209
10210 $addons = $this->get_addons();
10211
10212 if ( is_array( $addons ) ) {
10213 foreach ( $addons as $addon ) {
10214 if ( $id == $addon->id ) {
10215 return $addon;
10216 }
10217 }
10218 }
10219
10220 return false;
10221 }
10222
10223 /**
10224 * Get add-on by slug (from local data).
10225 *
10226 * @author Vova Feldman (@svovaf)
10227 * @since 1.0.6
10228 *
10229 * @param string $slug
10230 *
10231 * @param bool $flush
10232 *
10233 * @return FS_Plugin|false
10234 */
10235 function get_addon_by_slug( $slug, $flush = false ) {
10236 $this->_logger->entrance();
10237
10238 $addons = $this->get_addons( $flush );
10239
10240 if ( is_array( $addons ) ) {
10241 foreach ( $addons as $addon ) {
10242 if ( $slug === $addon->slug ) {
10243 return $addon;
10244 }
10245 }
10246 }
10247
10248 return false;
10249 }
10250
10251 /**
10252 * @var array<number,object[]> {
10253 * @key number Add-on ID.
10254 * @val object[] The add-on's plans and prices object.
10255 * }
10256 */
10257 private $plans_and_pricing_by_addon_id;
10258
10259 /**
10260 * @author Leo Fajardo (@leorw)
10261 * @since 2.3.0
10262 *
10263 * @return array<number,object[]> {
10264 * @key number Add-on ID.
10265 * @val object[] The add-on's plans and prices object.
10266 * }
10267 */
10268 function _get_addons_plans_and_pricing_map_by_id() {
10269 if ( ! isset( $this->plans_and_pricing_by_addon_id ) ) {
10270 $result = $this->get_api_plugin_scope()->get( $this->add_show_pending( "/addons/pricing.json?type=visible" ) );
10271
10272 $plans_and_pricing_by_addon_id = array();
10273 if ( $this->is_api_result_object( $result, 'addons' ) ) {
10274 foreach ( $result->addons as $addon ) {
10275 $plans_and_pricing_by_addon_id[ $addon->id ] = $addon->plans;
10276 }
10277 }
10278
10279 $this->plans_and_pricing_by_addon_id = $plans_and_pricing_by_addon_id;
10280 }
10281
10282 return $this->plans_and_pricing_by_addon_id;
10283 }
10284
10285 /**
10286 * @author Leo Fajardo (@leorw)
10287 * @since 2.3.0
10288 *
10289 * @param number $addon_id
10290 * @param bool $is_installed
10291 *
10292 * @return array
10293 */
10294 function _get_addon_info( $addon_id, $is_installed ) {
10295 $addon = $this->get_addon( $addon_id );
10296
10297 if ( ! is_object( $addon ) ) {
10298 // Unexpected call.
10299 return array();
10300 }
10301
10302 $slug = $addon->slug;
10303
10304 $addon_storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
10305
10306 if ( ! fs_is_network_admin() ) {
10307 // Get blog-level activated installations.
10308 $sites = self::$_accounts->get_option( 'sites', array() );
10309 } else {
10310 $sites = null;
10311
10312 if ( $this->is_addon_activated( $addon_id ) &&
10313 $this->get_addon_instance( $addon_id )->is_network_active()
10314 ) {
10315 if ( FS_Site::is_valid_id( $addon_storage->network_install_blog_id ) ) {
10316 // Get network-level activated installations.
10317 $sites = self::$_accounts->get_option(
10318 'sites',
10319 array(),
10320 $addon_storage->network_install_blog_id
10321 );
10322 }
10323 }
10324 }
10325
10326 $addon_info = array(
10327 'is_connected' => false,
10328 'slug' => $slug,
10329 'title' => $addon->title
10330 );
10331
10332 if ( ! $is_installed ) {
10333 $plans_and_pricing_by_addon_id = $this->_get_addons_plans_and_pricing_map_by_id();
10334
10335 if ( isset( $plans_and_pricing_by_addon_id[ $addon_id ] ) ) {
10336 $has_paid_plan = false;
10337 $plans = $plans_and_pricing_by_addon_id[ $addon_id ];
10338
10339 if ( is_array( $plans ) && count( $plans ) > 0 ) {
10340 foreach ( $plans as $plan ) {
10341 if ( isset( $plan->pricing ) &&
10342 is_array( $plan->pricing ) &&
10343 count( $plan->pricing ) > 0
10344 ) {
10345 $has_paid_plan = true;
10346 break;
10347 }
10348 }
10349 }
10350
10351 $addon_info['has_paid_plan'] = $has_paid_plan;
10352 }
10353 }
10354
10355 if ( ! is_array( $sites ) || ! isset( $sites[ $slug ] ) ) {
10356 return $addon_info;
10357 }
10358
10359 $site = $sites[ $slug ];
10360
10361 $addon_info['is_connected'] = (
10362 ( $addon->parent_plugin_id == $this->get_id() ) &&
10363 is_object( $site ) &&
10364 FS_Site::is_valid_id( $site->id ) &&
10365 FS_User::is_valid_id( $site->user_id ) &&
10366 FS_Plugin_Plan::is_valid_id( $site->plan_id )
10367 );
10368
10369 if ( $addon_info['is_connected'] && $is_installed ) {
10370 return $addon_info;
10371 }
10372
10373 $addon_info['site'] = $site;
10374
10375 $plugins_data = self::$_accounts->get_option( WP_FS__MODULE_TYPE_PLUGIN . 's', array() );
10376 if ( isset( $plugins_data[ $slug ] ) ) {
10377 $plugin_data = $plugins_data[ $slug ];
10378
10379 $addon_info['version'] = $plugin_data->version;
10380 }
10381
10382 $all_plans = self::$_accounts->get_option( 'plans', array() );
10383 if ( isset( $all_plans[ $slug ] ) ) {
10384 $plans = $all_plans[ $slug ];
10385
10386 foreach ( $plans as $plan ) {
10387 if ( $site->plan_id == Freemius::_decrypt( $plan->id ) ) {
10388 $addon_info['plan_name'] = Freemius::_decrypt( $plan->name );
10389 $addon_info['plan_title'] = Freemius::_decrypt( $plan->title );
10390 break;
10391 }
10392 }
10393 }
10394
10395 $licenses = self::$_accounts->get_option( 'all_licenses', array() );
10396 if ( is_array( $licenses ) && isset( $licenses[ $addon_id ] ) ) {
10397 foreach ( $licenses[ $addon_id ] as $license ) {
10398 if ( $license->id == $site->license_id ) {
10399 $addon_info['license'] = $license;
10400 break;
10401 }
10402 }
10403 }
10404
10405 if ( isset( $addon_info['license'] ) ) {
10406 if ( isset( $addon_storage->subscriptions ) &&
10407 ! empty( $addon_storage->subscriptions )
10408 ) {
10409 foreach ( $addon_storage->subscriptions as $subscription ) {
10410 if ( $subscription->license_id == $site->license_id ) {
10411 $addon_info['subscription'] = $subscription;
10412 break;
10413 }
10414 }
10415 }
10416 }
10417
10418 return $addon_info;
10419 }
10420
10421 /**
10422 * @author Vova Feldman (@svovaf)
10423 * @since 2.0.0
10424 *
10425 * @param number $user_id
10426 *
10427 * @return FS_User
10428 */
10429 static function _get_user_by_id( $user_id ) {
10430 self::$_static_logger->entrance( "user_id = {$user_id}" );
10431
10432 $users = self::get_all_users();
10433
10434 if ( is_array( $users ) ) {
10435 if ( isset( $users[ $user_id ] ) &&
10436 $users[ $user_id ] instanceof FS_User &&
10437 $user_id == $users[ $user_id ]->id
10438 ) {
10439 return $users[ $user_id ];
10440 }
10441
10442 // If user wasn't found by the key, iterate over all the users collection.
10443 foreach ( $users as $user ) {
10444 /**
10445 * @var FS_User $user
10446 */
10447 if ( $user_id == $user->id ) {
10448 return $user;
10449 }
10450 }
10451 }
10452
10453 return null;
10454 }
10455
10456 /**
10457 * Checks if a Freemius user_id is associated with a super-admin.
10458 *
10459 * @author Vova Feldman (@svovaf)
10460 * @since 2.0.0
10461 *
10462 * @param number $user_id
10463 *
10464 * @return bool
10465 */
10466 private static function is_super_admin( $user_id ) {
10467 $is_super_admin = false;
10468
10469 $user = self::_get_user_by_id( $user_id );
10470
10471 if ( $user instanceof FS_User && ! empty( $user->email ) ) {
10472 self::require_pluggable_essentials();
10473
10474 $wp_user = get_user_by( 'email', $user->email );
10475
10476 if ( $wp_user instanceof WP_User ) {
10477 $super_admins = get_super_admins();
10478 $is_super_admin = ( is_array( $super_admins ) && in_array( $wp_user->user_login, $super_admins ) );
10479 }
10480 }
10481
10482 return $is_super_admin;
10483 }
10484
10485 #----------------------------------------------------------------------------------
10486 #region Plans & Licensing
10487 #----------------------------------------------------------------------------------
10488
10489 /**
10490 * Check if running premium plugin code.
10491 *
10492 * @author Vova Feldman (@svovaf)
10493 * @since 1.0.5
10494 *
10495 * @return bool
10496 */
10497 function is_premium() {
10498 /**
10499 * `$this->_plugin` will be `false` when `is_activation_mode` calls this method directly from the
10500 * `_register_hooks` method.
10501 *
10502 * @author Leo Fajardo (@leorw)
10503 * @since 2.2.3
10504 */
10505 return is_object( $this->_plugin ) ?
10506 $this->_plugin->is_premium :
10507 false;
10508 }
10509
10510 /**
10511 * Get site's plan ID.
10512 *
10513 * @author Vova Feldman (@svovaf)
10514 * @since 1.0.2
10515 *
10516 * @return number
10517 */
10518 function get_plan_id() {
10519 return $this->_site->plan_id;
10520 }
10521
10522 /**
10523 * Get site's plan title.
10524 *
10525 * @author Vova Feldman (@svovaf)
10526 * @since 1.0.2
10527 *
10528 * @return string
10529 */
10530 function get_plan_title() {
10531 $plan = $this->get_plan();
10532
10533 return is_object( $plan ) ? $plan->title : 'PLAN_TITLE';
10534 }
10535
10536 /**
10537 * Get site's plan name.
10538 *
10539 * @author Vova Feldman (@svovaf)
10540 * @since 2.0.0
10541 *
10542 * @return string
10543 */
10544 function get_plan_name() {
10545 $plan = $this->get_plan();
10546
10547 return is_object( $plan ) ? $plan->name : 'PLAN_NAME';
10548 }
10549
10550 /**
10551 * @author Vova Feldman (@svovaf)
10552 * @since 1.0.9
10553 *
10554 * @return FS_Plugin_Plan|false
10555 */
10556 function get_plan() {
10557 if ( ! is_object( $this->_site ) ) {
10558 return false;
10559 }
10560
10561 return FS_Plugin_Plan::is_valid_id( $this->_site->plan_id ) ?
10562 $this->_get_plan_by_id( $this->_site->plan_id ) :
10563 false;
10564 }
10565
10566 /**
10567 * @author Vova Feldman (@svovaf)
10568 * @since 1.0.3
10569 *
10570 * @return bool
10571 */
10572 function is_trial() {
10573 $this->_logger->entrance();
10574
10575 if ( ! $this->is_registered() || ! is_object( $this->_site ) ) {
10576 return false;
10577 }
10578
10579 return $this->_site->is_trial();
10580 }
10581
10582 /**
10583 * Check if currently in a trial with payment method (credit card or paypal).
10584 *
10585 * @author Vova Feldman (@svovaf)
10586 * @since 1.1.7
10587 *
10588 * @return bool
10589 */
10590 function is_paid_trial() {
10591 $this->_logger->entrance();
10592
10593 if ( ! $this->is_trial() ) {
10594 return false;
10595 }
10596
10597 return $this->has_active_valid_license() && ( $this->_site->trial_plan_id == $this->_license->plan_id );
10598 }
10599
10600 /**
10601 * Check if trial already utilized.
10602 *
10603 * @since 1.0.9
10604 *
10605 * @return bool
10606 */
10607 function is_trial_utilized() {
10608 $this->_logger->entrance();
10609
10610 if ( ! $this->is_registered() ) {
10611 return false;
10612 }
10613
10614 return $this->_site->is_trial_utilized();
10615 }
10616
10617 /**
10618 * Get trial plan information (if in trial).
10619 *
10620 * @author Vova Feldman (@svovaf)
10621 * @since 1.0.9
10622 *
10623 * @return bool|FS_Plugin_Plan
10624 */
10625 function get_trial_plan() {
10626 $this->_logger->entrance();
10627
10628 if ( ! $this->is_trial() ) {
10629 return false;
10630 }
10631
10632 // Try to load plan from local cache.
10633 $trial_plan = $this->_get_plan_by_id( $this->_site->trial_plan_id );
10634
10635 if ( ! is_object( $trial_plan ) ) {
10636 $trial_plan = $this->_fetch_site_plan( $this->_site->trial_plan_id );
10637
10638 /**
10639 * If managed to fetch the plan, add it to the plans collection.
10640 */
10641 if ( $trial_plan instanceof FS_Plugin_Plan ) {
10642 if ( ! is_array( $this->_plans ) ) {
10643 $this->_plans = array();
10644 }
10645
10646 $this->_plans[] = $trial_plan;
10647 $this->_store_plans();
10648 }
10649 }
10650
10651 if ( $trial_plan instanceof FS_Plugin_Plan ) {
10652 return $trial_plan;
10653 }
10654
10655 /**
10656 * If for some reason failed to get the trial plan, fallback to a dummy name and title.
10657 */
10658 $trial_plan = new FS_Plugin_Plan();
10659 $trial_plan->id = $this->_site->trial_plan_id;
10660 $trial_plan->name = 'pro';
10661 $trial_plan->title = 'Pro';
10662
10663 return $trial_plan;
10664 }
10665
10666 /**
10667 * Check if the user has an activate, non-expired license on current plugin's install.
10668 *
10669 * @since 1.0.9
10670 *
10671 * @return bool
10672 */
10673 function is_paying() {
10674 $this->_logger->entrance();
10675
10676 if ( ! $this->is_registered() ) {
10677 return false;
10678 }
10679
10680 if ( ! $this->has_paid_plan() ) {
10681 return false;
10682 }
10683
10684 return (
10685 ! $this->is_trial() &&
10686 'free' !== $this->get_plan_name() &&
10687 $this->has_active_valid_license()
10688 );
10689 }
10690
10691 /**
10692 * @author Vova Feldman (@svovaf)
10693 * @since 1.0.4
10694 *
10695 * @return bool
10696 */
10697 function is_free_plan() {
10698 if ( ! $this->is_registered() ) {
10699 return true;
10700 }
10701
10702 if ( ! $this->has_paid_plan() ) {
10703 return true;
10704 }
10705
10706 return (
10707 'free' === $this->get_plan_name() ||
10708 ! $this->has_features_enabled_license()
10709 );
10710 }
10711
10712 /**
10713 * @author Vova Feldman (@svovaf)
10714 * @since 1.0.5
10715 *
10716 * @return bool
10717 */
10718 function _has_premium_license() {
10719 $this->_logger->entrance();
10720
10721 $premium_license = $this->_get_available_premium_license();
10722
10723 return ( false !== $premium_license );
10724 }
10725
10726 /**
10727 * Check if user has any licenses associated with the plugin (including expired or blocking).
10728 *
10729 * @author Vova Feldman (@svovaf)
10730 * @since 1.1.7.3
10731 *
10732 * @return bool
10733 */
10734 function has_any_license() {
10735 return is_array( $this->_licenses ) && ( 0 < count( $this->_licenses ) );
10736 }
10737
10738 /**
10739 * @author Vova Feldman (@svovaf)
10740 * @since 1.0.5
10741 *
10742 * @param bool|null $is_localhost
10743 *
10744 * @return FS_Plugin_License|false
10745 */
10746 function _get_available_premium_license( $is_localhost = null ) {
10747 $this->_logger->entrance();
10748
10749 $licenses = $this->get_available_premium_licenses( $is_localhost );
10750 if ( ! empty( $licenses ) ) {
10751 return $licenses[0];
10752 }
10753
10754 return false;
10755 }
10756
10757 /**
10758 * @author Vova Feldman (@svovaf)
10759 * @since 1.0.5
10760 *
10761 * @param bool|null $is_localhost
10762 *
10763 * @return FS_Plugin_License[]
10764 */
10765 function get_available_premium_licenses( $is_localhost = null ) {
10766 $this->_logger->entrance();
10767
10768 $licenses = array();
10769 if ( ! $this->has_paid_plan() ) {
10770 return $licenses;
10771 }
10772
10773 if ( is_array( $this->_licenses ) ) {
10774 foreach ( $this->_licenses as $license ) {
10775 if ( ! $license->can_activate( $is_localhost ) ) {
10776 continue;
10777 }
10778
10779 $licenses[] = $license;
10780 }
10781 }
10782
10783 return $licenses;
10784 }
10785
10786 /**
10787 * Sync local plugin plans with remote server.
10788 *
10789 * 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).
10790 *
10791 * @author Vova Feldman (@svovaf)
10792 * @since 1.0.5
10793 *
10794 * @return FS_Plugin_Plan[]|object
10795 */
10796 function _sync_plans() {
10797 $plans = $this->_fetch_plugin_plans();
10798
10799 if ( $this->is_array_instanceof( $plans, 'FS_Plugin_Plan' ) ) {
10800 $plans_map = array();
10801 foreach ( $plans as $plan ) {
10802 $plans_map[ $plan->id ] = true;
10803 }
10804
10805 $plans_ids_to_keep = $this->get_plans_ids_associated_with_installs();
10806
10807 foreach ( $plans_ids_to_keep as $plan_id ) {
10808 if ( isset( $plans_map[ $plan_id ] ) ) {
10809 continue;
10810 }
10811
10812 $missing_plan = self::_get_plan_by_id( $plan_id );
10813
10814 if ( is_object( $missing_plan ) ) {
10815 $plans[] = $missing_plan;
10816 }
10817 }
10818
10819 $this->_plans = $plans;
10820 $this->_store_plans();
10821 }
10822
10823 $this->do_action( 'after_plans_sync', $plans );
10824
10825 return $this->_plans;
10826 }
10827
10828 /**
10829 * Check if specified plan exists locally. If not, fetch it and store it.
10830 *
10831 * @author Vova Feldman (@svovaf)
10832 * @since 2.0.0
10833 *
10834 * @param number $plan_id
10835 *
10836 * @return \FS_Plugin_Plan|object The plan entity or the API error object on failure.
10837 */
10838 private function sync_plan_if_not_exist( $plan_id ) {
10839 $plan = self::_get_plan_by_id( $plan_id );
10840
10841 if ( is_object( $plan ) ) {
10842 // Plan already exists.
10843 return $plan;
10844 }
10845
10846 $plan = $this->fetch_plan_by_id( $plan_id );
10847
10848 if ( $plan instanceof FS_Plugin_Plan ) {
10849 $this->_plans[] = $plan;
10850 $this->_store_plans();
10851
10852 return $plan;
10853 }
10854
10855 return $plan;
10856 }
10857
10858 /**
10859 * Check if specified license exists locally. If not, fetch it and store it.
10860 *
10861 * @author Vova Feldman (@svovaf)
10862 * @since 2.0.0
10863 *
10864 * @param number $license_id
10865 * @param string $license_key
10866 *
10867 * @return \FS_Plugin_Plan|object The plan entity or the API error object on failure.
10868 */
10869 private function sync_license_if_not_exist( $license_id, $license_key ) {
10870 $license = $this->_get_license_by_id( $license_id );
10871
10872 if ( is_object( $license ) ) {
10873 // License already exists.
10874 return $license;
10875 }
10876
10877 $license = $this->fetch_license_by_key( $license_id, $license_key );
10878
10879 if ( $license instanceof FS_Plugin_License ) {
10880 $this->_licenses[] = $license;
10881 $this->_license = $license;
10882 $this->_store_licenses();
10883
10884 return $license;
10885 }
10886
10887 return $license;
10888 }
10889
10890 /**
10891 * Get a collection of unique plan IDs that are associated with any installs in the network.
10892 *
10893 * @author Leo Fajardo (@leorw)
10894 * @since 2.0.0
10895 *
10896 * @return number[]
10897 */
10898 private function get_plans_ids_associated_with_installs() {
10899 if ( ! is_multisite() ) {
10900 if ( ! is_object( $this->_site ) ||
10901 ! FS_Plugin_Plan::is_valid_id( $this->_site->plan_id )
10902 ) {
10903 return array();
10904 }
10905
10906 return array( $this->_site->plan_id );
10907 }
10908
10909 $plan_ids = array();
10910 $sites = self::get_sites();
10911 foreach ( $sites as $site ) {
10912 $blog_id = self::get_site_blog_id( $site );
10913 $install = $this->get_install_by_blog_id( $blog_id );
10914
10915 if ( ! is_object( $install ) ||
10916 ! FS_Plugin_Plan::is_valid_id( $install->plan_id )
10917 ) {
10918 continue;
10919 }
10920
10921 $plan_ids[ $install->plan_id ] = true;
10922 }
10923
10924 return array_keys( $plan_ids );
10925 }
10926
10927 /**
10928 * Get a collection of unique license IDs that are associated with any installs in the network.
10929 *
10930 * @author Leo Fajardo (@leorw)
10931 * @since 2.0.0
10932 *
10933 * @return number[]
10934 */
10935 private function get_license_ids_associated_with_installs() {
10936 if ( ! $this->_is_network_active ) {
10937 if ( ! is_object( $this->_site ) ||
10938 ! FS_Plugin_License::is_valid_id( $this->_site->license_id )
10939 ) {
10940 return array();
10941 }
10942
10943 return array( $this->_site->license_id );
10944 }
10945
10946 $license_ids = array();
10947 $sites = self::get_sites();
10948 foreach ( $sites as $site ) {
10949 $blog_id = self::get_site_blog_id( $site );
10950 $install = $this->get_install_by_blog_id( $blog_id );
10951
10952 if ( ! is_object( $install ) ||
10953 ! FS_Plugin_License::is_valid_id( $install->license_id )
10954 ) {
10955 continue;
10956 }
10957
10958 $license_ids[ $install->license_id ] = true;
10959 }
10960
10961 return array_keys( $license_ids );
10962 }
10963
10964 /**
10965 * @author Vova Feldman (@svovaf)
10966 * @since 1.0.5
10967 *
10968 * @param number $id
10969 *
10970 * @return FS_Plugin_Plan|false
10971 */
10972 function _get_plan_by_id( $id ) {
10973 $this->_logger->entrance();
10974
10975 if ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) {
10976 $this->_sync_plans();
10977 }
10978
10979 foreach ( $this->_plans as $plan ) {
10980 if ( $id == $plan->id ) {
10981 return $plan;
10982 }
10983 }
10984
10985 return false;
10986 }
10987
10988 /**
10989 * @author Vova Feldman (@svovaf)
10990 * @since 1.1.8.1
10991 *
10992 * @param string $name
10993 *
10994 * @return FS_Plugin_Plan|false
10995 */
10996 private function get_plan_by_name( $name ) {
10997 $this->_logger->entrance();
10998
10999 if ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) {
11000 $this->_sync_plans();
11001 }
11002
11003 foreach ( $this->_plans as $plan ) {
11004 if ( $name == $plan->name ) {
11005 return $plan;
11006 }
11007 }
11008
11009 return false;
11010 }
11011
11012 /**
11013 * Sync local licenses with remote server.
11014 *
11015 * @author Vova Feldman (@svovaf)
11016 * @since 1.0.6
11017 *
11018 * @param number|bool $site_license_id
11019 * @param number|null $blog_id
11020 *
11021 * @return FS_Plugin_License[]|object
11022 */
11023 function _sync_licenses( $site_license_id = false, $blog_id = null ) {
11024 $this->_logger->entrance();
11025
11026 $is_network_admin = fs_is_network_admin();
11027
11028 if ( $is_network_admin && is_null( $blog_id ) ) {
11029 $all_licenses = self::get_all_licenses( $this->_module_id );
11030 } else {
11031 $all_licenses = $this->get_user_licenses( $this->_user->id );
11032 }
11033
11034 $foreign_licenses = $this->get_foreign_licenses_info( $all_licenses, $site_license_id );
11035
11036 $all_licenses_map = array();
11037 foreach ( $all_licenses as $license ) {
11038 $all_licenses_map[ $license->id ] = true;
11039 }
11040
11041 $licenses = $this->_fetch_licenses( false, $site_license_id, $foreign_licenses, $blog_id );
11042
11043 if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) {
11044 $licenses_map = array();
11045 foreach ( $licenses as $license ) {
11046 $licenses_map[ $license->id ] = true;
11047 }
11048
11049 // $license_ids_to_keep = $this->get_license_ids_associated_with_installs();
11050 // foreach ( $license_ids_to_keep as $license_id ) {
11051 // if ( isset( $licenses_map[ $license_id ] ) ) {
11052 // continue;
11053 // }
11054 //
11055 // $missing_license = self::_get_license_by_id( $license_id, false );
11056 // if ( is_object( $missing_license ) ) {
11057 // $licenses[] = $missing_license;
11058 // $licenses_map[ $missing_license->id ] = true;
11059 // }
11060 // }
11061
11062 $user_license_ids = $this->get_user_linked_license_ids( $this->_user->id );
11063
11064 foreach ( $user_license_ids as $key => $license_id ) {
11065 if ( ! isset( $licenses_map[ $license_id ] ) ) {
11066 // Remove access to licenses that no longer exist.
11067 unset( $user_license_ids[ $key ] );
11068 }
11069 }
11070
11071 if ( ! empty( $user_license_ids ) ) {
11072 foreach ( $licenses_map as $license_id => $value ) {
11073 if ( ! isset( $all_licenses_map[ $license_id ] ) ) {
11074 // Associate new licenses with the user who triggered the license syncing.
11075 $user_license_ids[] = $license_id;
11076 }
11077 }
11078
11079 $user_license_ids = array_unique( $user_license_ids );
11080 } else {
11081 $user_license_ids = array_keys( $licenses_map );
11082 }
11083
11084 if ( ! $is_network_admin || ! is_null( $blog_id ) ) {
11085 $user_licenses = array();
11086 foreach ( $licenses as $license ) {
11087 if ( ! in_array( $license->id, $user_license_ids ) ) {
11088 continue;
11089 }
11090
11091 $user_licenses[] = $license;
11092 }
11093
11094 $this->_licenses = $user_licenses;
11095 } else {
11096 $this->_licenses = $licenses;
11097 }
11098
11099 $this->set_user_linked_license_ids( $this->_user->id, $user_license_ids );
11100
11101 $this->_store_licenses( true, $this->_module_id, $licenses );
11102 }
11103
11104 // Update current license.
11105 if ( is_object( $this->_license ) ) {
11106 $this->_license = $this->_get_license_by_id( $this->_license->id );
11107 }
11108
11109 return $this->_licenses;
11110 }
11111
11112 /**
11113 * @author Vova Feldman (@svovaf)
11114 * @since 1.0.5
11115 *
11116 * @param number $id
11117 * @param bool $sync_licenses
11118 *
11119 * @return FS_Plugin_License|false
11120 */
11121 function _get_license_by_id( $id, $sync_licenses = true ) {
11122 $this->_logger->entrance();
11123
11124 if ( ! FS_Plugin_License::is_valid_id( $id ) ) {
11125 return false;
11126 }
11127
11128 /**
11129 * When running from the network level admin and opted-in from the network,
11130 * check if the license exists in the network user licenses collection.
11131 *
11132 * @author Vova Feldman (@svovaf)
11133 * @since 2.0.0
11134 */
11135 if ( fs_is_network_admin() &&
11136 $this->is_network_registered() &&
11137 ( ! is_object( $this->_user ) || $this->_storage->network_user_id != $this->_user->id )
11138 ) {
11139 $licenses = $this->get_user_licenses( $this->_storage->network_user_id );
11140
11141 foreach ( $licenses as $license ) {
11142 if ( $id == $license->id ) {
11143 return $license;
11144 }
11145 }
11146 }
11147
11148 if ( ! $this->has_any_license() && $sync_licenses ) {
11149 $this->_sync_licenses( $id );
11150 }
11151
11152 if ( is_array( $this->_licenses ) ) {
11153 foreach ( $this->_licenses as $license ) {
11154 if ( $id == $license->id ) {
11155 return $license;
11156 }
11157 }
11158 }
11159
11160 return false;
11161 }
11162
11163 /**
11164 * 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.
11165 *
11166 * @author Vova Feldman (@svovaf)
11167 * @since 2.0.0
11168 *
11169 * @param number $id
11170 *
11171 * @return FS_Plugin_License
11172 */
11173 private function get_license_by_id( $id ) {
11174 $licenses = self::get_all_licenses( $this->_module_id );
11175
11176 if ( is_array( $licenses ) && ! empty( $licenses ) ) {
11177 foreach ( $licenses as $license ) {
11178 if ( $id == $license->id ) {
11179 return $license;
11180 }
11181 }
11182 }
11183
11184 return null;
11185 }
11186
11187 /**
11188 * Synchronize the site's context license by fetching the license form the API and updating the local data with it.
11189 *
11190 * @author Vova Feldman (@svovaf)
11191 * @since 2.0.0
11192 *
11193 * @return \FS_Plugin_License|mixed
11194 */
11195 private function sync_site_license() {
11196 $api = $this->get_api_user_scope();
11197
11198 $result = $api->get( "/licenses/{$this->_license->id}.json?license_key=" . urlencode( $this->_license->secret_key ), true );
11199
11200 if ( ! $this->is_api_result_entity( $result ) ) {
11201 return $result;
11202 }
11203
11204 $license = $this->_update_site_license( new FS_Plugin_License( $result ) );
11205 $this->_store_licenses();
11206
11207 return $license;
11208 }
11209
11210 /**
11211 * Get all user's available licenses for the current module.
11212 *
11213 * @author Vova Feldman (@svovaf)
11214 * @since 2.0.0
11215 *
11216 * @param number $user_id
11217 *
11218 * @return FS_Plugin_License[]
11219 */
11220 private function get_user_licenses( $user_id ) {
11221 $all_licenses = self::get_all_licenses( $this->_module_id );
11222 if ( empty( $all_licenses ) ) {
11223 return array();
11224 }
11225
11226 $user_license_ids = $this->get_user_linked_license_ids( $user_id );
11227 if ( empty( $user_license_ids ) ) {
11228 return array();
11229 }
11230
11231 $licenses = array();
11232 foreach ( $all_licenses as $license ) {
11233 if ( in_array( $license->id, $user_license_ids ) ) {
11234 $licenses[] = $license;
11235 }
11236 }
11237
11238 return $licenses;
11239 }
11240
11241 /**
11242 * Checks if the context license is network activated except on the given blog ID.
11243 *
11244 * @author Vova Feldman (@svovaf)
11245 * @since 2.0.0
11246 *
11247 * @param int $except_blog_id
11248 *
11249 * @return bool
11250 */
11251 private function is_license_network_active( $except_blog_id = 0 ) {
11252 $this->_logger->entrance();
11253
11254 if ( ! is_object( $this->_license ) ) {
11255 return false;
11256 }
11257
11258 $sites = self::get_sites();
11259
11260 if ( $this->_license->total_activations() < ( count( $sites ) - 1 ) ) {
11261 // There are more sites than the number of activations, so license cannot be network activated.
11262 return false;
11263 }
11264
11265 foreach ( $sites as $site ) {
11266 $blog_id = self::get_site_blog_id( $site );
11267
11268 if ( $except_blog_id == $blog_id ) {
11269 // Skip excluded blog.
11270 continue;
11271 }
11272
11273 $install = $this->get_install_by_blog_id( $blog_id );
11274
11275 if ( is_object( $install ) && $install->license_id != $this->_license->id ) {
11276 return false;
11277 }
11278 }
11279
11280 return true;
11281 }
11282
11283 /**
11284 * 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.
11285 *
11286 * Notice: On success, this method will also update the license activations counters (without updating the license in the storage).
11287 *
11288 * @author Vova Feldman (@svovaf)
11289 * @since 2.0.0
11290 *
11291 * @param \FS_User $user
11292 * @param \FS_Plugin_License $license
11293 *
11294 * @return bool
11295 */
11296 private function try_activate_license_on_network( FS_User $user, FS_Plugin_License $license ) {
11297 $this->_logger->entrance();
11298
11299 $result = $this->can_activate_license_on_network( $license );
11300
11301 if ( false === $result ) {
11302 return false;
11303 }
11304
11305 $installs_without_license = $result['installs'];
11306 if ( ! empty( $installs_without_license ) ) {
11307 $this->activate_license_on_many_installs( $user, $license->secret_key, $installs_without_license );
11308 }
11309
11310 $disconnected_site_ids = $result['sites'];
11311 if ( ! empty( $disconnected_site_ids ) ) {
11312 $this->activate_license_on_many_sites( $user, $license->secret_key, $disconnected_site_ids );
11313 }
11314
11315 $this->link_license_2_user( $license->id, $user->id );
11316
11317 // Sync license after activations.
11318 $license->activated += $result['production_count'];
11319 $license->activated_local += $result['localhost_count'];
11320
11321 // $this->_store_licenses()
11322
11323 return true;
11324 }
11325
11326 /**
11327 * Checks if the given license can be activated on the whole network.
11328 *
11329 * @author Vova Feldman (@svovaf)
11330 * @since 2.0.0
11331 *
11332 * @param \FS_Plugin_License $license
11333 *
11334 * @return false|array {
11335 * @type array[int]FS_Site $installs Blog ID to install map.
11336 * @type int[] $sites Non-connected blog IDs.
11337 * @type int $production_count Production sites count.
11338 * @type int $localhost_count Production sites count.
11339 * }
11340 */
11341 private function can_activate_license_on_network( FS_Plugin_License $license ) {
11342 $sites = self::get_sites();
11343
11344 $production_count = 0;
11345 $localhost_count = 0;
11346
11347 $installs_without_license = array();
11348 $disconnected_site_ids = array();
11349
11350 foreach ( $sites as $site ) {
11351 $blog_id = self::get_site_blog_id( $site );
11352 $install = $this->get_install_by_blog_id( $blog_id );
11353
11354 if ( is_object( $install ) ) {
11355 if ( FS_Plugin_License::is_valid_id( $install->license_id ) ) {
11356 // License already activated on the install.
11357 continue;
11358 }
11359
11360 $url = $install->url;
11361
11362 $installs_without_license[ $blog_id ] = $install;
11363 } else {
11364 $url = is_object( $site ) ?
11365 $site->siteurl :
11366 get_site_url( $blog_id );
11367
11368 $disconnected_site_ids[] = $blog_id;
11369 }
11370
11371 if ( FS_Site::is_localhost_by_address( $url ) ) {
11372 $localhost_count ++;
11373 } else {
11374 $production_count ++;
11375 }
11376 }
11377
11378 if ( ! $license->can_activate_bulk( $production_count, $localhost_count ) ) {
11379 return false;
11380 }
11381
11382 return array(
11383 'installs' => $installs_without_license,
11384 'sites' => $disconnected_site_ids,
11385 'production_count' => $production_count,
11386 'localhost_count' => $localhost_count,
11387 );
11388 }
11389
11390 /**
11391 * Activate a given license on a collection of installs.
11392 *
11393 * @author Vova Feldman (@svovaf)
11394 * @since 2.0.0
11395 *
11396 * @param \FS_User $user
11397 * @param string $license_key
11398 * @param array $blog_2_install_map {
11399 * @key int Blog ID.
11400 * @value FS_Site Blog's associated install.
11401 * }
11402 *
11403 * @return mixed|true
11404 */
11405 private function activate_license_on_many_installs(
11406 FS_User $user,
11407 $license_key,
11408 array $blog_2_install_map
11409 ) {
11410 $params = array(
11411 array( 'license_key' => $this->apply_filters( 'license_key', $license_key ) )
11412 );
11413
11414 $install_2_blog_map = array();
11415 foreach ( $blog_2_install_map as $blog_id => $install ) {
11416 $params[] = array( 'id' => $install->id );
11417
11418 $install_2_blog_map[ $install->id ] = $blog_id;
11419 }
11420
11421 $result = $this->get_api_user_scope_by_user( $user )->call(
11422 "plugins/{$this->_plugin->id}/installs.json",
11423 'PUT',
11424 $params
11425 );
11426
11427 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
11428 return $result;
11429 }
11430
11431 foreach ( $result->installs as $r_install ) {
11432 $install = new FS_Site( $r_install );
11433 $install->is_disconnected = false;
11434
11435 // Update install.
11436 $this->_store_site(
11437 true,
11438 $install_2_blog_map[ $r_install->id ],
11439 $install
11440 );
11441 }
11442
11443 return true;
11444 }
11445
11446 /**
11447 * Activate a given license on a collection of blogs/sites that are not yet opted-in.
11448 *
11449 * @author Vova Feldman (@svovaf)
11450 * @since 2.0.0
11451 *
11452 * @param \FS_User $user
11453 * @param string $license_key
11454 * @param int[] $site_ids
11455 *
11456 * @return true|mixed True if successful, otherwise, the API result.
11457 */
11458 private function activate_license_on_many_sites(
11459 FS_User $user,
11460 $license_key,
11461 array $site_ids
11462 ) {
11463 $sites = array();
11464 foreach ( $site_ids as $site_id ) {
11465 $sites[] = $this->get_site_info( array( 'blog_id' => $site_id ) );
11466 }
11467
11468 // Install the plugin.
11469 $result = $this->create_installs_with_user(
11470 $user,
11471 $license_key,
11472 false,
11473 $sites,
11474 false,
11475 true
11476 );
11477
11478 if ( ! $this->is_api_result_entity( $result ) &&
11479 ! $this->is_api_result_object( $result, 'installs' )
11480 ) {
11481 return $result;
11482 }
11483
11484 $installs = array();
11485 foreach ( $result->installs as $install ) {
11486 $installs[] = new FS_Site( $install );
11487 }
11488
11489 // Map site addresses to their blog IDs.
11490 $address_to_blog_map = $this->get_address_to_blog_map();
11491
11492 $first_blog_id = null;
11493
11494 foreach ( $installs as $install ) {
11495 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
11496 $blog_id = $address_to_blog_map[ $address ];
11497
11498 $this->_store_site( true, $blog_id, $install );
11499
11500 $this->reset_anonymous_mode( $blog_id );
11501
11502 if ( is_null( $first_blog_id ) ) {
11503 $first_blog_id = $blog_id;
11504 }
11505 }
11506
11507 if ( ! FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ) {
11508 $this->_storage->network_install_blog_id = $first_blog_id;
11509 }
11510
11511 return true;
11512 }
11513
11514 /**
11515 * Sync site's license with user licenses.
11516 *
11517 * @author Vova Feldman (@svovaf)
11518 * @since 1.0.6
11519 *
11520 * @param FS_Plugin_License|null $new_license
11521 *
11522 * @return FS_Plugin_License|null
11523 */
11524 function _update_site_license( $new_license ) {
11525 $this->_logger->entrance();
11526
11527 $this->_license = $new_license;
11528
11529 if ( ! is_object( $new_license ) ) {
11530 $this->_site->license_id = null;
11531 $this->_sync_site_subscription( null );
11532
11533 return $this->_license;
11534 }
11535
11536 $this->_site->license_id = $this->_license->id;
11537
11538 if ( ! is_array( $this->_licenses ) ) {
11539 $this->_licenses = array();
11540 }
11541
11542 $is_license_found = false;
11543 for ( $i = 0, $len = count( $this->_licenses ); $i < $len; $i ++ ) {
11544 if ( $new_license->id == $this->_licenses[ $i ]->id ) {
11545 $this->_licenses[ $i ] = $new_license;
11546
11547 $is_license_found = true;
11548 break;
11549 }
11550 }
11551
11552 // If new license just append.
11553 if ( ! $is_license_found ) {
11554 $this->_licenses[] = $new_license;
11555 }
11556
11557 $this->_sync_site_subscription( $new_license );
11558
11559 return $this->_license;
11560 }
11561
11562 /**
11563 * Sync site's subscription.
11564 *
11565 * @author Vova Feldman (@svovaf)
11566 * @since 1.0.9
11567 *
11568 * @param FS_Plugin_License|null $license
11569 *
11570 * @return bool|\FS_Subscription
11571 */
11572 private function _sync_site_subscription( $license ) {
11573 if ( ! is_object( $license ) ) {
11574 $this->delete_unused_subscriptions();
11575
11576 return false;
11577 }
11578
11579 // Load subscription details if not lifetime.
11580 $subscription = $license->is_lifetime() ?
11581 false :
11582 $this->_fetch_site_license_subscription();
11583
11584 if ( is_object( $subscription ) && ! isset( $subscription->error ) ) {
11585 $this->store_subscription( $subscription );
11586 } else {
11587 $this->delete_unused_subscriptions();
11588 }
11589
11590 return $subscription;
11591 }
11592
11593 /**
11594 * @author Vova Feldman (@svovaf)
11595 * @since 1.0.6
11596 *
11597 * @return bool|\FS_Plugin_License
11598 */
11599 function _get_license() {
11600 if ( ! fs_is_network_admin() || is_object( $this->_license ) ) {
11601 return $this->_license;
11602 }
11603
11604 return $this->_get_available_premium_license();
11605 }
11606
11607 /**
11608 * @param number $license_id
11609 *
11610 * @return null|\FS_Subscription
11611 */
11612 function _get_subscription( $license_id ) {
11613 if ( ! isset( $this->_storage->subscriptions ) ||
11614 empty( $this->_storage->subscriptions )
11615 ) {
11616 return null;
11617 }
11618
11619 foreach ( $this->_storage->subscriptions as $subscription ) {
11620 if ( $subscription->license_id == $license_id ) {
11621 return $subscription;
11622 }
11623 }
11624
11625 return null;
11626 }
11627
11628 /**
11629 * @author Leo Fajardo (@leorw)
11630 * @since 2.0.0
11631 *
11632 * @param FS_Subscription $subscription
11633 */
11634 function store_subscription( FS_Subscription $subscription ) {
11635 if ( ! isset( $this->_storage->subscriptions ) ) {
11636 $this->_storage->subscriptions = array();
11637 }
11638
11639 if ( empty( $this->_storage->subscriptions ) || ! is_multisite() ) {
11640 $this->_storage->subscriptions = array( $subscription );
11641
11642 return;
11643 }
11644
11645 $subscriptions = $this->_storage->subscriptions;
11646
11647 $updated_subscription = false;
11648 foreach ( $subscriptions as $key => $existing_subscription ) {
11649 if ( $existing_subscription->id == $subscription->id ) {
11650 $subscriptions[ $key ] = $subscription;
11651 $updated_subscription = true;
11652 break;
11653 }
11654 }
11655
11656 if ( ! $updated_subscription ) {
11657 $subscriptions[] = $subscription;
11658 }
11659
11660 $this->_storage->subscriptions = $subscriptions;
11661 }
11662
11663 /**
11664 * @author Leo Fajardo (@leorw)
11665 * @since 2.0.0
11666 */
11667 function delete_unused_subscriptions() {
11668 if ( ! isset( $this->_storage->subscriptions ) ||
11669 empty( $this->_storage->subscriptions ) ||
11670 // Clean up only if there are already at least 3 subscriptions.
11671 ( count( $this->_storage->subscriptions ) < 3 )
11672 ) {
11673 return;
11674 }
11675
11676 if ( ! is_multisite() ) {
11677 // If not multisite, there should only be 1 subscription, so just clear the array.
11678 $this->_storage->subscriptions = array();
11679
11680 return;
11681 }
11682
11683 $subscriptions_to_keep_by_license_id_map = array();
11684 $sites = self::get_sites();
11685 foreach ( $sites as $site ) {
11686 $blog_id = self::get_site_blog_id( $site );
11687 $install = $this->get_install_by_blog_id( $blog_id );
11688
11689 if ( ! is_object( $install ) ||
11690 ! FS_Plugin_License::is_valid_id( $install->license_id )
11691 ) {
11692 continue;
11693 }
11694
11695 $subscriptions_to_keep_by_license_id_map[ $install->license_id ] = true;
11696 }
11697
11698 if ( empty( $subscriptions_to_keep_by_license_id_map ) ) {
11699 $this->_storage->subscriptions = array();
11700
11701 return;
11702 }
11703
11704 foreach ( $this->_storage->subscriptions as $key => $subscription ) {
11705 if ( ! isset( $subscriptions_to_keep_by_license_id_map[ $subscription->license_id ] ) ) {
11706 unset( $this->_storage->subscriptions[ $key ] );
11707 }
11708 }
11709 }
11710
11711 /**
11712 * @author Vova Feldman (@svovaf)
11713 * @since 1.0.2
11714 *
11715 * @param string $plan Plan name
11716 * @param bool $exact If true, looks for exact plan. If false, also check "higher" plans.
11717 *
11718 * @return bool
11719 */
11720 function is_plan( $plan, $exact = false ) {
11721 $this->_logger->entrance();
11722
11723 if ( ! $this->is_registered() ) {
11724 return false;
11725 }
11726
11727 $plan = strtolower( $plan );
11728
11729 $current_plan_name = $this->get_plan_name();
11730
11731 if ( $current_plan_name === $plan ) {
11732 // Exact plan.
11733 return true;
11734 } else if ( $exact ) {
11735 // Required exact, but plans are different.
11736 return false;
11737 }
11738
11739 $current_plan_order = - 1;
11740 $required_plan_order = - 1;
11741 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
11742 if ( $plan === $this->_plans[ $i ]->name ) {
11743 $required_plan_order = $i;
11744 } else if ( $current_plan_name === $this->_plans[ $i ]->name ) {
11745 $current_plan_order = $i;
11746 }
11747 }
11748
11749 return ( $current_plan_order > $required_plan_order );
11750 }
11751
11752 /**
11753 * Check if module has only one plan.
11754 *
11755 * @author Vova Feldman (@svovaf)
11756 * @since 1.2.1.7
11757 *
11758 * @return bool
11759 */
11760 function is_single_plan() {
11761 $this->_logger->entrance();
11762
11763 if ( ! $this->is_registered() ||
11764 ! is_array( $this->_plans ) ||
11765 0 === count( $this->_plans )
11766 ) {
11767 return true;
11768 }
11769
11770 return ( 1 === ( count( $this->_plans ) - ( $this->has_free_plan() ? 1 : 0 ) ) );
11771 }
11772
11773 /**
11774 * Check if plan based on trial. If not in trial mode, should return false.
11775 *
11776 * @since 1.0.9
11777 *
11778 * @param string $plan Plan name
11779 * @param bool $exact If true, looks for exact plan. If false, also check "higher" plans.
11780 *
11781 * @return bool
11782 */
11783 function is_trial_plan( $plan, $exact = false ) {
11784 $this->_logger->entrance();
11785
11786 if ( ! $this->is_registered() ) {
11787 return false;
11788 }
11789
11790 if ( ! $this->is_trial() ) {
11791 return false;
11792 }
11793
11794 $trial_plan = $this->get_trial_plan();
11795
11796 if ( $trial_plan->name === $plan ) {
11797 // Exact plan.
11798 return true;
11799 } else if ( $exact ) {
11800 // Required exact, but plans are different.
11801 return false;
11802 }
11803
11804 $current_plan_order = - 1;
11805 $required_plan_order = - 1;
11806 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
11807 if ( $plan === $this->_plans[ $i ]->name ) {
11808 $required_plan_order = $i;
11809 } else if ( $trial_plan->name === $this->_plans[ $i ]->name ) {
11810 $current_plan_order = $i;
11811 }
11812 }
11813
11814 return ( $current_plan_order > $required_plan_order );
11815 }
11816
11817 /**
11818 * Check if plugin has any paid plans.
11819 *
11820 * @author Vova Feldman (@svovaf)
11821 * @since 1.0.7
11822 *
11823 * @return bool
11824 */
11825 function has_paid_plan() {
11826 return $this->_has_paid_plans ||
11827 FS_Plan_Manager::instance()->has_paid_plan( $this->_plans );
11828 }
11829
11830 /**
11831 * Check if plugin has any plan with a trail.
11832 *
11833 * @author Vova Feldman (@svovaf)
11834 * @since 1.0.9
11835 *
11836 * @return bool
11837 */
11838 function has_trial_plan() {
11839 /**
11840 * @author Vova Feldman(@svovaf)
11841 * @since 1.2.1.5
11842 *
11843 * Allow setting a trial from the SDK without calling the API.
11844 * But, if the user did opt-in, continue using the real data from the API.
11845 */
11846 if ( $this->_trial_days >= 0 ) {
11847 return true;
11848 }
11849
11850 return $this->_storage->get( 'has_trial_plan', false );
11851 }
11852
11853 /**
11854 * Check if plugin has any free plan, or is it premium only.
11855 *
11856 * Note: If no plans configured, assume plugin is free.
11857 *
11858 * @author Vova Feldman (@svovaf)
11859 * @since 1.0.7
11860 *
11861 * @return bool
11862 */
11863 function has_free_plan() {
11864 return ! $this->is_only_premium();
11865 }
11866
11867 /**
11868 * Displays a license activation dialog box when the user clicks on the "Activate License"
11869 * or "Change License" link on the plugins
11870 * page.
11871 *
11872 * @author Leo Fajardo (@leorw)
11873 * @since 1.1.9
11874 */
11875 function _add_license_activation_dialog_box() {
11876 $vars = array(
11877 'id' => $this->_module_id,
11878 );
11879
11880 fs_require_template( 'forms/license-activation.php', $vars );
11881 fs_require_template( 'forms/resend-key.php', $vars );
11882 }
11883
11884 /**
11885 * Displays a subscription cancellation dialog box when the user clicks on the "Deactivate License"
11886 * link on the "Account" page or deactivates a plugin and there's an active subscription that is
11887 * either associated with a non-lifetime single-site license or non-lifetime multisite license that
11888 * is only activated on a single production site.
11889 *
11890 * @author Leo Fajardo (@leorw)
11891 * @since 2.2.1
11892 *
11893 * @param bool $is_license_deactivation
11894 *
11895 * @return array
11896 */
11897 function _get_subscription_cancellation_dialog_box_template_params( $is_license_deactivation = false ) {
11898 if ( fs_is_network_admin() ) {
11899 // Subscription cancellation dialog box is currently not supported for multisite networks.
11900 return array();
11901 }
11902
11903 $license = $this->_get_license();
11904
11905 /**
11906 * 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.
11907 *
11908 * @author Leo Fajardo (@leorw) (Comment added by Vova Feldman @svovaf)
11909 * @since 2.2.1
11910 */
11911 if ( ! is_object( $license ) ||
11912 $license->is_lifetime() ||
11913 ( ! $license->is_single_site() && $license->activated > 1 )
11914 ) {
11915 return array();
11916 }
11917
11918 /**
11919 * @var FS_Subscription $subscription
11920 */
11921 $subscription = $this->_get_subscription( $license->id );
11922 if ( ! is_object( $subscription ) || ! $subscription->is_active() ) {
11923 return array();
11924 }
11925
11926 return array(
11927 'id' => $this->_module_id,
11928 'license' => $license,
11929 'has_trial' => $this->is_paid_trial(),
11930 'is_license_deactivation' => $is_license_deactivation,
11931 );
11932 }
11933
11934 /**
11935 * @author Leo Fajardo (@leorw)
11936 * @since 2.0.2
11937 */
11938 function _add_premium_version_upgrade_selection_dialog_box() {
11939 $modules_update = get_site_transient( $this->is_theme() ? 'update_themes' : 'update_plugins' );
11940 if ( ! isset( $modules_update->response[ $this->_plugin_basename ] ) ) {
11941 return;
11942 }
11943
11944 $vars = array(
11945 'id' => $this->_module_id,
11946 'new_version' => is_object( $modules_update->response[ $this->_plugin_basename ] ) ?
11947 $modules_update->response[ $this->_plugin_basename ]->new_version :
11948 $modules_update->response[ $this->_plugin_basename ]['new_version']
11949 );
11950
11951 fs_require_template( 'forms/premium-versions-upgrade-metadata.php', $vars );
11952 fs_require_once_template( 'forms/premium-versions-upgrade-handler.php', $vars );
11953 }
11954
11955 /**
11956 * Displays the opt-out dialog box when the user clicks on the "Opt Out" link on the "Plugins"
11957 * page.
11958 *
11959 * @author Leo Fajardo (@leorw)
11960 * @since 1.2.1.5
11961 */
11962 function _add_optout_dialog() {
11963 if ( $this->is_theme() ) {
11964 $vars = null;
11965 fs_require_once_template( '/js/jquery.content-change.php', $vars );
11966 }
11967
11968 $vars = array( 'id' => $this->_module_id );
11969 fs_require_template( 'forms/optout.php', $vars );
11970 }
11971
11972 /**
11973 * Prepare page to include all required UI and logic for the license activation dialog.
11974 *
11975 * @author Vova Feldman (@svovaf)
11976 * @since 1.2.0
11977 */
11978 function _add_license_activation() {
11979 if ( ! $this->is_user_admin() ) {
11980 // Only admins can activate a license.
11981 return;
11982 }
11983
11984 if ( ! $this->has_paid_plan() ) {
11985 // Module doesn't have any paid plans.
11986 return;
11987 }
11988
11989 if ( ! $this->is_premium() ) {
11990 // Only add license activation logic to the premium version.
11991 return;
11992 }
11993
11994 // Add license activation link and AJAX request handler.
11995 if ( self::is_plugins_page() ) {
11996 $is_network_admin = fs_is_network_admin();
11997
11998 if (
11999 ( $is_network_admin && $this->is_network_active() && ! $this->is_network_delegated_connection() ) ||
12000 ( ! $is_network_admin && ( ! $this->is_network_active() || $this->is_delegated_connection() ) )
12001 ) {
12002 /**
12003 * @since 1.2.0 Add license action link only on plugins page.
12004 */
12005 $this->_add_license_action_link();
12006 }
12007 }
12008
12009 // Add license activation AJAX callback.
12010 $this->add_ajax_action( 'activate_license', array( &$this, '_activate_license_ajax_action' ) );
12011
12012 // Add resend license AJAX callback.
12013 $this->add_ajax_action( 'resend_license_key', array( &$this, '_resend_license_key_ajax_action' ) );
12014 }
12015
12016 /**
12017 * @author Leo Fajardo (@leorw)
12018 * @since 2.0.2
12019 */
12020 function _add_premium_version_upgrade_selection() {
12021 if ( ! $this->is_user_admin() ) {
12022 return;
12023 }
12024
12025 if ( ! $this->is_premium() || $this->has_any_active_valid_license() ) {
12026 // This is relevant only to the free versions and premium versions without an active license.
12027 return;
12028 }
12029
12030 if ( self::is_updates_page() || ( $this->is_plugin() && self::is_plugins_page() ) ) {
12031 $this->_add_premium_version_upgrade_selection_action();
12032 }
12033 }
12034
12035 /**
12036 * @author Leo Fajardo (@leorw)
12037 * @since 2.3.0
12038 */
12039 function _add_beta_mode_update_handler() {
12040 if ( ! $this->is_user_admin() ) {
12041 return;
12042 }
12043
12044 if ( ! $this->is_premium() ) {
12045 return;
12046 }
12047
12048 $this->add_ajax_action( 'set_beta_mode', array( &$this, '_set_beta_mode_ajax_handler' ) );
12049 }
12050
12051 /**
12052 * @author Leo Fajardo (@leorw)
12053 * @since 2.3.0
12054 */
12055 function _set_beta_mode_ajax_handler() {
12056 $this->_logger->entrance();
12057
12058 $this->check_ajax_referer( 'set_beta_mode' );
12059
12060 if ( ! $this->is_user_admin() ) {
12061 // Only for admins.
12062 self::shoot_ajax_failure();
12063 }
12064
12065 $is_beta = trim( fs_request_get( 'is_beta', '', 'post' ) );
12066
12067 if ( empty( $is_beta ) || ! in_array( $is_beta, array( 'true', 'false' ) ) ) {
12068 self::shoot_ajax_failure();
12069 }
12070
12071 $user = $this->get_api_user_scope()->call(
12072 '',
12073 'put',
12074 array(
12075 'plugin_id' => $this->get_id(),
12076 'is_beta' => ( 'true' == $is_beta ),
12077 'fields' => 'is_beta'
12078 )
12079 );
12080
12081 if ( ! $this->is_api_result_entity( $user ) ) {
12082 self::shoot_ajax_failure(
12083 FS_Api::is_api_error_object( $user ) ?
12084 $user->error->message :
12085 fs_text_inline( "An unknown error has occurred while trying to set the user's beta mode.", 'unknown-error-occurred', $this->get_slug() )
12086 );
12087 }
12088
12089 $this->_user->is_beta = $user->is_beta;
12090 $this->_store_user();
12091
12092 self::shoot_ajax_response( array( 'success' => true ) );
12093 }
12094
12095 /**
12096 * License activation WP AJAX handler.
12097 *
12098 * @author Leo Fajardo (@leorw)
12099 * @since 1.1.9
12100 *
12101 * @uses Freemius::activate_license()
12102 */
12103 function _activate_license_ajax_action() {
12104 $this->_logger->entrance();
12105
12106 $this->check_ajax_referer( 'activate_license' );
12107
12108 $license_key = trim( fs_request_get( 'license_key' ) );
12109
12110 if ( empty( $license_key ) ) {
12111 exit;
12112 }
12113
12114 $result = $this->activate_license(
12115 $license_key,
12116 fs_is_network_admin() ?
12117 fs_request_get( 'sites', array(), 'post' ) :
12118 array(),
12119 fs_request_get_bool( 'is_marketing_allowed', null ),
12120 fs_request_get( 'blog_id', null ),
12121 fs_request_get( 'module_id', null, 'post' )
12122 );
12123
12124 echo json_encode( $result );
12125
12126 exit;
12127 }
12128
12129 /**
12130 * A helper method to activate migrated licenses. If the product is network activated and integrated, the method will network activate the license.
12131 *
12132 * @author Vova Feldman (@svovaf)
12133 * @since 2.3.0
12134 *
12135 * @param string $license_key
12136 * @param null|bool $is_marketing_allowed
12137 * @param null|number $plugin_id
12138 *
12139 * @return array {
12140 * @var bool $success
12141 * @var string $error
12142 * @var string $next_page
12143 * }
12144 *
12145 * @uses Freemius::activate_license()
12146 */
12147 function activate_migrated_license(
12148 $license_key,
12149 $is_marketing_allowed = null,
12150 $plugin_id = null
12151 ) {
12152 return $this->activate_license(
12153 $license_key,
12154 $this->is_network_active() ?
12155 $this->get_sites_for_network_level_optin() :
12156 array(),
12157 $is_marketing_allowed,
12158 null,
12159 $plugin_id
12160 );
12161 }
12162
12163 /**
12164 * The implementation of this method was previously in `_activate_license_ajax_action()`.
12165 *
12166 * @author Vova Feldman (@svovaf)
12167 * @since 2.2.4
12168 * @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).
12169 * @param string $license_key
12170 * @param array $sites
12171 * @param null|bool $is_marketing_allowed
12172 * @param null|int $blog_id
12173 * @param null|number $plugin_id
12174 *
12175 * @return array {
12176 * @var bool $success
12177 * @var string $error
12178 * @var string $next_page
12179 * }
12180 */
12181 private function activate_license(
12182 $license_key,
12183 $sites = array(),
12184 $is_marketing_allowed = null,
12185 $blog_id = null,
12186 $plugin_id = null
12187 ) {
12188 $this->_logger->entrance();
12189
12190 $license_key = trim( $license_key );
12191
12192 if ( ! fs_is_network_admin() ) {
12193 // If the license activation is executed outside the context of a network admin, ignore the sites collection.
12194 $sites = array();
12195 }
12196
12197 $fs = ( empty($plugin_id) || $plugin_id == $this->_module_id ) ?
12198 $this :
12199 $this->get_addon_instance( $plugin_id );
12200
12201 $error = false;
12202 $next_page = false;
12203
12204 $has_valid_blog_id = is_numeric( $blog_id );
12205
12206 if ( $fs->is_registered() ) {
12207 if ( fs_is_network_admin() && ! $has_valid_blog_id ) {
12208 // If no specific blog ID was provided, activate the license for all sites in the network.
12209 $blog_2_install_map = array();
12210 $site_ids = array();
12211
12212 foreach ( $sites as $site ) {
12213 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
12214 continue;
12215 }
12216
12217 $install = $this->get_install_by_blog_id( $site['blog_id'] );
12218
12219 if ( is_object( $install ) ) {
12220 $blog_2_install_map[ $site['blog_id'] ] = $install;
12221 } else {
12222 $site_ids[] = $site['blog_id'];
12223 }
12224 }
12225
12226 $user = $this->get_current_or_network_user();
12227
12228 if ( ! empty( $blog_2_install_map ) ) {
12229 $result = $this->activate_license_on_many_installs( $user, $license_key, $blog_2_install_map );
12230
12231 if ( true !== $result ) {
12232 $error = FS_Api::is_api_error_object( $result ) ?
12233 $result->error->message :
12234 var_export( $result, true );
12235 }
12236 }
12237
12238 if ( empty( $error ) && ! empty( $site_ids ) ) {
12239 $result = $this->activate_license_on_many_sites( $user, $license_key, $site_ids );
12240
12241 if ( true !== $result ) {
12242 $error = FS_Api::is_api_error_object( $result ) ?
12243 $result->error->message :
12244 var_export( $result, true );
12245 }
12246 }
12247 } else {
12248 if ( $has_valid_blog_id ) {
12249 /**
12250 * If a specific blog ID was provided, activate the license only for the install that is
12251 * associated with the given blog ID.
12252 *
12253 * @author Leo Fajardo (@leorw)
12254 */
12255 $this->switch_to_blog( $blog_id );
12256 }
12257
12258 $api = $fs->get_api_site_scope();
12259
12260 $params = array(
12261 'license_key' => $fs->apply_filters( 'license_key', $license_key )
12262 );
12263
12264 $install = $api->call( $fs->add_show_pending( '/' ), 'put', $params );
12265
12266 if ( FS_Api::is_api_error( $install ) ) {
12267 $error = FS_Api::is_api_error_object( $install ) ?
12268 $install->error->message :
12269 var_export( $install->error, true );
12270 } else {
12271 $fs->reconnect_locally( $has_valid_blog_id );
12272 }
12273 }
12274
12275 if ( empty( $error ) ) {
12276 $this->network_upgrade_mode_completed();
12277
12278 $fs->_sync_license( true, $has_valid_blog_id );
12279
12280 $next_page = $fs->is_addon() ?
12281 $fs->get_parent_instance()->get_account_url() :
12282 $fs->get_account_url();
12283 }
12284 } else {
12285 $next_page = $fs->opt_in(
12286 false,
12287 false,
12288 false,
12289 $license_key,
12290 false,
12291 false,
12292 false,
12293 $is_marketing_allowed,
12294 $sites
12295 );
12296
12297 if ( isset( $next_page->error ) ) {
12298 $error = $next_page->error;
12299 } else {
12300 if ( fs_is_network_admin() ) {
12301 /**
12302 * Get the list of sites that were just opted-in (and license activated).
12303 * This is an optimization for the next part below saving some DB queries.
12304 */
12305 $connected_sites = array();
12306 foreach ( $sites as $site ) {
12307 if ( isset( $site['blog_id'] ) && is_numeric( $site['blog_id'] ) ) {
12308 $connected_sites[ $site['blog_id'] ] = true;
12309 }
12310 }
12311
12312 $all_sites = self::get_sites();
12313 $pending_sites = array();
12314
12315 /**
12316 * 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.
12317 *
12318 * @author Vova Feldman (@svovaf)
12319 */
12320 foreach ( $all_sites as $site ) {
12321 $blog_id = self::get_site_blog_id( $site );
12322
12323 if ( isset( $connected_sites[ $blog_id ] ) ) {
12324 // Site was just connected.
12325 continue;
12326 }
12327
12328 if ( $this->is_installed_on_site( $blog_id ) ) {
12329 // Site was already connected before.
12330 continue;
12331 }
12332
12333 if ( $this->is_site_delegated_connection( $blog_id ) ) {
12334 // Site's connection was delegated.
12335 continue;
12336 }
12337
12338 if ( $this->is_anonymous_site( $blog_id ) ) {
12339 // Site connection was already skipped.
12340 continue;
12341 }
12342
12343 $pending_sites[] = self::get_site_info( $site );
12344 }
12345
12346 if ( ! empty( $pending_sites ) ) {
12347 if ( $this->is_freemium() && $this->is_enable_anonymous() ) {
12348 $this->skip_connection( $pending_sites );
12349 } else {
12350 $this->delegate_connection( $pending_sites );
12351 }
12352 }
12353 }
12354 }
12355 }
12356
12357 if ( false === $error && true === $this->_storage->require_license_activation ) {
12358 $this->_storage->require_license_activation = false;
12359 }
12360
12361 $result = array(
12362 'success' => ( false === $error )
12363 );
12364
12365 if ( false !== $error ) {
12366 $result['error'] = $this->apply_filters( 'opt_in_error_message', $error );
12367 } else {
12368 if ( $this->is_addon() || $this->has_addons() ) {
12369 /**
12370 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
12371 * an updated valid user licenses collection will be fetched from the server which is used to also
12372 * update the account add-ons (add-ons the user has licenses for).
12373 *
12374 * @author Leo Fajardo (@leorw)
12375 * @since 2.2.4
12376 */
12377 $this->purge_valid_user_licenses_cache();
12378 }
12379
12380 $result['next_page'] = $next_page;
12381 }
12382
12383 return $result;
12384 }
12385
12386 /**
12387 * @author Leo Fajardo (@leorw)
12388 * @since 1.2.3.1
12389 */
12390 function _network_activate_ajax_action() {
12391 $this->_logger->entrance();
12392
12393 $this->check_ajax_referer( 'network_activate' );
12394
12395 $plugin_id = fs_request_get( 'module_id', '', 'post' );
12396 $fs = ( $plugin_id == $this->_module_id ) ?
12397 $this :
12398 $this->get_addon_instance( $plugin_id );
12399
12400 $error = false;
12401
12402 $sites = fs_request_get( 'sites', array(), 'post' );
12403 if ( is_array( $sites ) && ! empty( $sites ) ) {
12404 $sites_by_action = array(
12405 'allow' => array(),
12406 'delegate' => array(),
12407 'skip' => array()
12408 );
12409
12410 foreach ( $sites as $site ) {
12411 $sites_by_action[ $site['action'] ][] = $site;
12412 }
12413
12414 $total_sites = count( $sites );
12415 $total_sites_to_delegate = count( $sites_by_action['delegate'] );
12416
12417 $next_page = '';
12418
12419 $has_any_install = fs_request_get_bool( 'has_any_install' );
12420
12421 if ( $total_sites === $total_sites_to_delegate &&
12422 ! $this->is_network_upgrade_mode() &&
12423 ! $has_any_install
12424 ) {
12425 $this->delegate_connection();
12426 } else {
12427 if ( ! empty( $sites_by_action['delegate'] ) ) {
12428 $this->delegate_connection( $sites_by_action['delegate'] );
12429 }
12430
12431 if ( ! empty( $sites_by_action['skip'] ) ) {
12432 $this->skip_connection( $sites_by_action['skip'] );
12433 }
12434
12435 if ( empty( $sites_by_action['allow'] ) ) {
12436 if ( $has_any_install ) {
12437 $first_install = $fs->find_first_install();
12438
12439 if ( ! is_null( $first_install ) ) {
12440 $fs->_site = $first_install['install'];
12441 $fs->_storage->network_install_blog_id = $first_install['blog_id'];
12442
12443 $fs->_user = self::_get_user_by_id( $fs->_site->user_id );
12444 $fs->_storage->network_user_id = $fs->_user->id;
12445 }
12446 }
12447 } else {
12448 if ( ! $fs->is_registered() || ! $this->_is_network_active ) {
12449 $next_page = $fs->opt_in(
12450 false,
12451 false,
12452 false,
12453 false,
12454 false,
12455 false,
12456 false,
12457 fs_request_get_bool( 'is_marketing_allowed', null ),
12458 $sites_by_action['allow']
12459 );
12460 } else {
12461 $next_page = $fs->install_with_user(
12462 $this->get_network_user(),
12463 false,
12464 false,
12465 false,
12466 true,
12467 $sites_by_action['allow']
12468 );
12469 }
12470
12471 if ( is_object( $next_page ) && isset( $next_page->error ) ) {
12472 $error = $next_page->error;
12473 }
12474 }
12475 }
12476
12477 if ( empty( $next_page ) ) {
12478 $next_page = $this->get_after_activation_url( 'after_network_activation_url' );
12479 }
12480 } else {
12481 $error = $this->get_text_inline( 'Invalid site details collection.', 'invalid_site_details_collection' );
12482 }
12483
12484 $result = array(
12485 'success' => ( false === $error )
12486 );
12487
12488 if ( false !== $error ) {
12489 $result['error'] = $error;
12490 } else {
12491 $result['next_page'] = $next_page;
12492 }
12493
12494 echo json_encode( $result );
12495
12496 exit;
12497 }
12498
12499 /**
12500 * Billing update AJAX callback.
12501 *
12502 * @author Vova Feldman (@svovaf)
12503 * @since 1.2.1.5
12504 */
12505 function _update_billing_ajax_action() {
12506 $this->_logger->entrance();
12507
12508 $this->check_ajax_referer( 'update_billing' );
12509
12510 if ( ! $this->is_user_admin() ) {
12511 // Only for admins.
12512 self::shoot_ajax_failure();
12513 }
12514
12515 $billing = fs_request_get( 'billing' );
12516
12517 $api = $this->get_api_user_scope();
12518 $result = $api->call( '/billing.json', 'put', array_merge( $billing, array(
12519 'plugin_id' => $this->get_parent_id(),
12520 ) ) );
12521
12522 if ( ! $this->is_api_result_entity( $result ) ) {
12523 self::shoot_ajax_failure();
12524 }
12525
12526 // Purge cached billing.
12527 $this->get_api_user_scope()->purge_cache( 'billing.json' );
12528
12529 self::shoot_ajax_success();
12530 }
12531
12532 /**
12533 * Trial start for anonymous users (AJAX callback).
12534 *
12535 * @author Vova Feldman (@svovaf)
12536 * @since 1.2.1.5
12537 */
12538 function _start_trial_ajax_action() {
12539 $this->_logger->entrance();
12540
12541 $this->check_ajax_referer( 'start_trial' );
12542
12543 if ( ! $this->is_user_admin() ) {
12544 // Only for admins.
12545 self::shoot_ajax_failure();
12546 }
12547
12548 $trial_data = fs_request_get( 'trial' );
12549
12550 $next_page = $this->opt_in(
12551 false,
12552 false,
12553 false,
12554 false,
12555 false,
12556 $trial_data['plan_id']
12557 );
12558
12559 if ( is_object( $next_page ) && $this->is_api_error( $next_page ) ) {
12560 self::shoot_ajax_failure(
12561 isset( $next_page->error ) ?
12562 $next_page->error->message :
12563 var_export( $next_page, true )
12564 );
12565 }
12566
12567 $this->shoot_ajax_success( array(
12568 'next_page' => $next_page,
12569 ) );
12570 }
12571
12572 /**
12573 * @author Leo Fajardo (@leorw)
12574 * @since 1.2.0
12575 */
12576 function _resend_license_key_ajax_action() {
12577 $this->_logger->entrance();
12578
12579 $this->check_ajax_referer( 'resend_license_key' );
12580
12581 $email_address = sanitize_email( trim( fs_request_get( 'email', '', 'post' ) ) );
12582
12583 if ( empty( $email_address ) ) {
12584 exit;
12585 }
12586
12587 $error = false;
12588
12589 $api = $this->get_api_plugin_scope();
12590 $result = $api->call( '/licenses/resend.json', 'post',
12591 array(
12592 'email' => $email_address,
12593 'url' => home_url(),
12594 )
12595 );
12596
12597 if ( is_object( $result ) && isset( $result->error ) ) {
12598 $error = $result->error;
12599
12600 if ( in_array( $error->code, array( 'invalid_email', 'no_user' ) ) ) {
12601 $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' );
12602 } else if ( 'no_license' === $error->code ) {
12603 $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' );
12604 } else {
12605 $error = $error->message;
12606 }
12607 }
12608
12609 $licenses = array(
12610 'success' => ( false === $error )
12611 );
12612
12613 if ( false !== $error ) {
12614 $licenses['error'] = sprintf( '%s... %s', $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ), strtolower( $error ) );
12615 }
12616
12617 echo json_encode( $licenses );
12618
12619 exit;
12620 }
12621
12622 /**
12623 * @author Vova Feldman (@svovaf)
12624 * @since 1.2.1.8
12625 *
12626 * @var string
12627 */
12628 private static $_pagenow;
12629
12630 /**
12631 * Get current page or the referer if executing a WP AJAX request.
12632 *
12633 * @author Vova Feldman (@svovaf)
12634 * @since 1.2.1.8
12635 *
12636 * @return string
12637 */
12638 static function get_current_page() {
12639 if ( ! isset( self::$_pagenow ) ) {
12640 global $pagenow;
12641 if ( empty( $pagenow ) && is_admin() && is_multisite() ) {
12642 /**
12643 * It appears that `$pagenow` is not yet initialized in some network admin pages when this method
12644 * is called, so initialize it here using some pieces of code from `wp-includes/vars.php`.
12645 *
12646 * @author Leo Fajardo (@leorw)
12647 * @since 2.2.3
12648 */
12649 if ( is_network_admin() ) {
12650 preg_match( '#/wp-admin/network/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
12651 } else if ( is_user_admin() ) {
12652 preg_match( '#/wp-admin/user/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
12653 } else {
12654 preg_match( '#/wp-admin/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
12655 }
12656
12657 $pagenow = $self_matches[1];
12658 $pagenow = trim( $pagenow, '/' );
12659 $pagenow = preg_replace( '#\?.*?$#', '', $pagenow );
12660 if ( '' === $pagenow || 'index' === $pagenow || 'index.php' === $pagenow ) {
12661 $pagenow = 'index.php';
12662 } else {
12663 preg_match( '#(.*?)(/|$)#', $pagenow, $self_matches );
12664 $pagenow = strtolower( $self_matches[1] );
12665 if ( '.php' !== substr($pagenow, -4, 4) )
12666 $pagenow .= '.php'; // for Options +Multiviews: /wp-admin/themes/index.php (themes.php is queried)
12667 }
12668 }
12669
12670 self::$_pagenow = $pagenow;
12671
12672 if ( self::is_ajax() &&
12673 'admin-ajax.php' === $pagenow
12674 ) {
12675 $referer = fs_get_raw_referer();
12676
12677 if ( is_string( $referer ) ) {
12678 $parts = explode( '?', $referer );
12679
12680 self::$_pagenow = basename( $parts[0] );
12681 }
12682 }
12683 }
12684
12685 return self::$_pagenow;
12686 }
12687
12688 /**
12689 * Helper method to check if user in the plugins page.
12690 *
12691 * @author Vova Feldman (@svovaf)
12692 * @since 1.2.1.5
12693 *
12694 * @return bool
12695 */
12696 static function is_plugins_page() {
12697 return ( 'plugins.php' === self::get_current_page() );
12698 }
12699
12700 /**
12701 * @author Leo Fajardo (@leorw)
12702 * @since 2.2.3
12703 *
12704 * @return bool
12705 */
12706 static function is_plugin_install_page() {
12707 return ( 'plugin-install.php' === self::get_current_page() );
12708 }
12709
12710 /**
12711 * @author Leo Fajardo (@leorw)
12712 * @since 2.0.2
12713 *
12714 * @return bool
12715 */
12716 static function is_updates_page() {
12717 return ( 'update-core.php' === self::get_current_page() );
12718 }
12719
12720 /**
12721 * Helper method to check if user in the themes page.
12722 *
12723 * @author Vova Feldman (@svovaf)
12724 * @since 1.2.2.6
12725 *
12726 * @return bool
12727 */
12728 static function is_themes_page() {
12729 return ( 'themes.php' === self::get_current_page() );
12730 }
12731
12732 #----------------------------------------------------------------------------------
12733 #region Affiliation
12734 #----------------------------------------------------------------------------------
12735
12736 /**
12737 * @author Leo Fajardo (@leorw)
12738 * @since 1.2.3
12739 *
12740 * @return bool
12741 */
12742 function has_affiliate_program() {
12743 if ( ! is_object( $this->_plugin ) ) {
12744 return false;
12745 }
12746
12747 return $this->_plugin->has_affiliate_program();
12748 }
12749
12750 /**
12751 * @author Leo Fajardo (@leorw)
12752 * @since 1.2.4
12753 */
12754 private function fetch_affiliate_terms() {
12755 if ( ! is_object( $this->plugin_affiliate_terms ) ) {
12756 $plugins_api = $this->get_api_plugin_scope();
12757 $affiliate_terms = $plugins_api->get( '/aff.json?type=affiliation', false );
12758
12759 if ( ! $this->is_api_result_entity( $affiliate_terms ) ) {
12760 return;
12761 }
12762
12763 $this->plugin_affiliate_terms = new FS_AffiliateTerms( $affiliate_terms );
12764 }
12765 }
12766
12767 /**
12768 * @author Leo Fajardo (@leorw)
12769 * @since 1.2.4
12770 */
12771 private function fetch_affiliate_and_custom_terms() {
12772 if ( ! empty( $this->_storage->affiliate_application_data ) ) {
12773 $application_data = $this->_storage->affiliate_application_data;
12774 $flush = ( ! isset( $application_data['status'] ) || 'pending' === $application_data['status'] );
12775
12776 $users_api = $this->get_api_user_scope();
12777 $result = $users_api->get( "/plugins/{$this->_plugin->id}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json", $flush );
12778 if ( $this->is_api_result_object( $result, 'affiliates' ) ) {
12779 if ( ! empty( $result->affiliates ) ) {
12780 $affiliate = new FS_Affiliate( $result->affiliates[0] );
12781
12782 if ( ! isset( $application_data['status'] ) || $application_data['status'] !== $affiliate->status ) {
12783 $application_data['status'] = $affiliate->status;
12784 $this->_storage->affiliate_application_data = $application_data;
12785 }
12786
12787 if ( $affiliate->is_using_custom_terms ) {
12788 $affiliate_terms = $users_api->get( "/plugins/{$this->_plugin->id}/affiliates/{$affiliate->id}/aff/{$affiliate->custom_affiliate_terms_id}.json", $flush );
12789 if ( $this->is_api_result_entity( $affiliate_terms ) ) {
12790 $this->custom_affiliate_terms = new FS_AffiliateTerms( $affiliate_terms );
12791 }
12792 }
12793
12794 $this->affiliate = $affiliate;
12795 }
12796 }
12797 }
12798 }
12799
12800 /**
12801 * @author Leo Fajardo (@leorw)
12802 * @since 1.2.3
12803 */
12804 private function fetch_affiliate_and_terms() {
12805 $this->_logger->entrance();
12806
12807 $this->fetch_affiliate_terms();
12808 $this->fetch_affiliate_and_custom_terms();
12809 }
12810
12811 /**
12812 * @author Leo Fajardo (@leorw)
12813 * @since 1.2.3
12814 *
12815 * @return FS_Affiliate
12816 */
12817 function get_affiliate() {
12818 return $this->affiliate;
12819 }
12820
12821
12822 /**
12823 * @author Leo Fajardo (@leorw)
12824 * @since 1.2.3
12825 *
12826 * @return FS_AffiliateTerms
12827 */
12828 function get_affiliate_terms() {
12829 return is_object( $this->custom_affiliate_terms ) ?
12830 $this->custom_affiliate_terms :
12831 $this->plugin_affiliate_terms;
12832 }
12833
12834 /**
12835 * @author Leo Fajardo (@leorw)
12836 * @since 1.2.3
12837 */
12838 function _submit_affiliate_application() {
12839 $this->_logger->entrance();
12840
12841 $this->check_ajax_referer( 'submit_affiliate_application' );
12842
12843 if ( ! $this->is_user_admin() ) {
12844 // Only for admins.
12845 self::shoot_ajax_failure();
12846 }
12847
12848 $affiliate = fs_request_get( 'affiliate' );
12849
12850 if ( empty( $affiliate['promotion_methods'] ) ) {
12851 unset( $affiliate['promotion_methods'] );
12852 }
12853
12854 if ( ! empty( $affiliate['additional_domains'] ) ) {
12855 $affiliate['additional_domains'] = array_unique( $affiliate['additional_domains'] );
12856 }
12857
12858 if ( ! $this->is_registered() ) {
12859 // Opt in but don't track usage.
12860 $next_page = $this->opt_in(
12861 false,
12862 false,
12863 false,
12864 false,
12865 false,
12866 false,
12867 true
12868 );
12869
12870 if ( is_object( $next_page ) && $this->is_api_error( $next_page ) ) {
12871 self::shoot_ajax_failure(
12872 isset( $next_page->error ) ?
12873 $next_page->error->message :
12874 var_export( $next_page, true )
12875 );
12876 } else if ( $this->is_pending_activation() ) {
12877 self::shoot_ajax_failure( $this->get_text_inline( 'Account is pending activation.', 'account-is-pending-activation' ) );
12878 }
12879 }
12880
12881 $this->fetch_affiliate_terms();
12882
12883 $api = $this->get_api_user_scope();
12884 $result = $api->call(
12885 ( "/plugins/{$this->_plugin->id}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json" ),
12886 'post',
12887 $affiliate
12888 );
12889
12890 if ( $this->is_api_error( $result ) ) {
12891 self::shoot_ajax_failure(
12892 isset( $result->error ) ?
12893 $result->error->message :
12894 var_export( $result, true )
12895 );
12896 } else {
12897 if ( $this->_admin_notices->has_sticky( 'affiliate_program' ) ) {
12898 $this->_admin_notices->remove_sticky( 'affiliate_program' );
12899 }
12900
12901 $affiliate_application_data = array(
12902 'status' => 'pending',
12903 'stats_description' => $affiliate['stats_description'],
12904 'promotion_method_description' => $affiliate['promotion_method_description'],
12905 );
12906
12907 if ( ! empty( $affiliate['promotion_methods'] ) ) {
12908 $affiliate_application_data['promotion_methods'] = $affiliate['promotion_methods'];
12909 }
12910
12911 if ( ! empty( $affiliate['domain'] ) ) {
12912 $affiliate_application_data['domain'] = $affiliate['domain'];
12913 }
12914
12915 if ( ! empty( $affiliate['additional_domains'] ) ) {
12916 $affiliate_application_data['additional_domains'] = $affiliate['additional_domains'];
12917 }
12918
12919 $this->_storage->affiliate_application_data = $affiliate_application_data;
12920 }
12921
12922 // Purge cached affiliate.
12923 $api->purge_cache( 'affiliate.json' );
12924
12925 self::shoot_ajax_success( $result );
12926 }
12927
12928 /**
12929 * @author Leo Fajardo (@leorw)
12930 * @since 1.2.3
12931 *
12932 * @return array|null
12933 */
12934 function get_affiliate_application_data() {
12935 if ( empty( $this->_storage->affiliate_application_data ) ) {
12936 return null;
12937 }
12938
12939 return $this->_storage->affiliate_application_data;
12940 }
12941
12942 #endregion Affiliation ------------------------------------------------------------
12943
12944 #----------------------------------------------------------------------------------
12945 #region URL Generators
12946 #----------------------------------------------------------------------------------
12947
12948 /**
12949 * Alias to pricing_url().
12950 *
12951 * @author Vova Feldman (@svovaf)
12952 * @since 1.0.2
12953 *
12954 * @uses pricing_url()
12955 *
12956 * @param string $period Billing cycle
12957 * @param bool $is_trial
12958 *
12959 * @return string
12960 */
12961 function get_upgrade_url( $period = WP_FS__PERIOD_ANNUALLY, $is_trial = false ) {
12962 return $this->pricing_url( $period, $is_trial );
12963 }
12964
12965 /**
12966 * @author Vova Feldman (@svovaf)
12967 * @since 1.0.9
12968 *
12969 * @uses get_upgrade_url()
12970 *
12971 * @return string
12972 */
12973 function get_trial_url() {
12974 return $this->get_upgrade_url( WP_FS__PERIOD_ANNUALLY, true );
12975 }
12976
12977 /**
12978 * @author Leo Fajardo (@leorw)
12979 * @since 2.1.4
12980 *
12981 * @param string $new_version
12982 *
12983 * @return string
12984 */
12985 function version_upgrade_checkout_link( $new_version ) {
12986 if ( ! is_object( $this->_license ) ) {
12987 $url = $this->pricing_url();
12988
12989 $purchase_license_text = $this->get_text_inline( 'Buy a license now', 'buy-license-now' );
12990 } else {
12991 $subscription = $this->_get_subscription( $this->_license->id );
12992
12993 $url = $this->checkout_url(
12994 is_object( $subscription ) ?
12995 ( 1 == $subscription->billing_cycle ? WP_FS__PERIOD_MONTHLY : WP_FS__PERIOD_ANNUALLY ) :
12996 WP_FS__PERIOD_LIFETIME,
12997 false,
12998 array( 'licenses' => $this->_license->quota )
12999 );
13000
13001 $purchase_license_text = $this->get_text_inline( 'Renew your license now', 'renew-license-now' );
13002 }
13003
13004 return sprintf(
13005 $this->get_text_inline( '%s to access version %s security & feature updates, and support.', 'x-for-updates-and-support' ),
13006 sprintf( '<a href="%s">%s</a>', $url, $purchase_license_text ),
13007 $new_version
13008 );
13009 }
13010
13011 /**
13012 * Plugin's pricing URL.
13013 *
13014 * @author Vova Feldman (@svovaf)
13015 * @since 1.0.4
13016 *
13017 * @param string $billing_cycle Billing cycle
13018 *
13019 * @param bool $is_trial
13020 *
13021 * @return string
13022 */
13023 function pricing_url( $billing_cycle = WP_FS__PERIOD_ANNUALLY, $is_trial = false ) {
13024 $this->_logger->entrance();
13025
13026 $params = array(
13027 'billing_cycle' => $billing_cycle
13028 );
13029
13030 if ( $is_trial ) {
13031 $params['trial'] = 'true';
13032 }
13033
13034 $url = $this->is_addon() ?
13035 $this->_parent->addon_url( $this->_slug ) :
13036 $this->_get_admin_page_url( 'pricing', $params );
13037
13038 return $this->apply_filters( 'pricing_url', $url );
13039 }
13040
13041 /**
13042 * Checkout page URL.
13043 *
13044 * @author Vova Feldman (@svovaf)
13045 * @since 1.0.6
13046 *
13047 * @param string $billing_cycle Billing cycle
13048 * @param bool $is_trial
13049 * @param array $extra (optional) Extra parameters, override other query params.
13050 * @param bool|null $network
13051 *
13052 * @return string
13053 */
13054 function checkout_url(
13055 $billing_cycle = WP_FS__PERIOD_ANNUALLY,
13056 $is_trial = false,
13057 $extra = array(),
13058 $network = null
13059 ) {
13060 $this->_logger->entrance();
13061
13062 $params = array(
13063 'checkout' => 'true',
13064 'billing_cycle' => $billing_cycle,
13065 );
13066
13067 if ( $is_trial ) {
13068 $params['trial'] = 'true';
13069 }
13070
13071 /**
13072 * Params in extra override other params.
13073 */
13074 $params = array_merge( $params, $extra );
13075
13076 return $this->_get_admin_page_url( 'pricing', $params, $network );
13077 }
13078
13079 /**
13080 * Add-on checkout URL.
13081 *
13082 * @author Vova Feldman (@svovaf)
13083 * @since 1.1.7
13084 *
13085 * @param number $addon_id
13086 * @param number $pricing_id
13087 * @param string $billing_cycle
13088 * @param bool $is_trial
13089 * @param bool|null $network
13090 *
13091 * @return string
13092 */
13093 function addon_checkout_url(
13094 $addon_id,
13095 $pricing_id,
13096 $billing_cycle = WP_FS__PERIOD_ANNUALLY,
13097 $is_trial = false,
13098 $network = null
13099 ) {
13100 return $this->checkout_url( $billing_cycle, $is_trial, array(
13101 'plugin_id' => $addon_id,
13102 'pricing_id' => $pricing_id,
13103 ), $network );
13104 }
13105
13106 #endregion
13107
13108 #endregion ------------------------------------------------------------------
13109
13110 /**
13111 * Check if plugin has any add-ons.
13112 *
13113 * @author Vova Feldman (@svovaf)
13114 * @since 1.0.5
13115 *
13116 * @since 1.1.7.3 Base logic only on the parameter provided by the developer in the init function.
13117 *
13118 * @return bool
13119 */
13120 function has_addons() {
13121 $this->_logger->entrance();
13122
13123 return $this->_has_addons;
13124 }
13125
13126 /**
13127 * Check if plugin can work in anonymous mode.
13128 *
13129 * @author Vova Feldman (@svovaf)
13130 * @since 1.0.9
13131 *
13132 * @return bool
13133 *
13134 * @deprecated Please use is_enable_anonymous() instead.
13135 */
13136 function enable_anonymous() {
13137 return $this->_enable_anonymous;
13138 }
13139
13140 /**
13141 * Check if plugin can work in anonymous mode.
13142 *
13143 * @author Vova Feldman (@svovaf)
13144 * @since 1.1.9
13145 *
13146 * @return bool
13147 */
13148 function is_enable_anonymous() {
13149 return $this->_enable_anonymous;
13150 }
13151
13152 /**
13153 * Check if plugin is premium only (no free plans).
13154 *
13155 * @author Vova Feldman (@svovaf)
13156 * @since 1.1.9
13157 *
13158 * @return bool
13159 */
13160 function is_only_premium() {
13161 return $this->_is_premium_only;
13162 }
13163
13164 /**
13165 * Checks if the plugin's type is "plugin". The other type is "theme".
13166 *
13167 * @author Leo Fajardo (@leorw)
13168 * @since 1.2.2
13169 *
13170 * @return bool
13171 */
13172 function is_plugin() {
13173 return ( WP_FS__MODULE_TYPE_PLUGIN === $this->_module_type );
13174 }
13175
13176 /**
13177 * @author Leo Fajardo (@leorw)
13178 * @since 1.2.2
13179 *
13180 * @return string
13181 */
13182 function get_module_type() {
13183 if ( ! isset( $this->_module_type ) ) {
13184 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
13185 $this->_module_type = $id_slug_type_path_map[ $this->_module_id ]['type'];
13186 }
13187
13188 return $this->_module_type;
13189 }
13190
13191 /**
13192 * @author Leo Fajardo (@leorw)
13193 * @since 1.2.2
13194 *
13195 * @return string
13196 */
13197 function get_plugin_main_file_path() {
13198 return $this->_plugin_main_file_path;
13199 }
13200
13201 /**
13202 * Check if module has a premium code version.
13203 *
13204 * Serviceware module might be freemium without any
13205 * premium code version, where the paid features
13206 * are all part of the service.
13207 *
13208 * @author Vova Feldman (@svovaf)
13209 * @since 1.2.1.6
13210 *
13211 * @return bool
13212 */
13213 function has_premium_version() {
13214 return $this->_has_premium_version;
13215 }
13216
13217 /**
13218 * Check if feature supported with current site's plan.
13219 *
13220 * @author Vova Feldman (@svovaf)
13221 * @since 1.0.1
13222 *
13223 * @todo IMPLEMENT
13224 *
13225 * @param number $feature_id
13226 *
13227 * @throws Exception
13228 */
13229 function is_feature_supported( $feature_id ) {
13230 throw new Exception( 'not implemented' );
13231 }
13232
13233 /**
13234 * @author Vova Feldman (@svovaf)
13235 * @since 1.0.1
13236 *
13237 * @return bool Is running in SSL/HTTPS
13238 */
13239 function is_ssl() {
13240 return WP_FS__IS_HTTPS;
13241 }
13242
13243 /**
13244 * @author Vova Feldman (@svovaf)
13245 * @since 1.0.9
13246 *
13247 * @return bool Is running in AJAX call.
13248 *
13249 * @link http://wordpress.stackexchange.com/questions/70676/how-to-check-if-i-am-in-admin-ajax
13250 */
13251 static function is_ajax() {
13252 return ( defined( 'DOING_AJAX' ) && DOING_AJAX );
13253 }
13254
13255 /**
13256 * Check if it's an AJAX call targeted for the current module.
13257 *
13258 * @author Vova Feldman (@svovaf)
13259 * @since 1.2.0
13260 *
13261 * @param array|string $actions Collection of AJAX actions.
13262 *
13263 * @return bool
13264 */
13265 function is_ajax_action( $actions ) {
13266 // Verify it's an ajax call.
13267 if ( ! self::is_ajax() ) {
13268 return false;
13269 }
13270
13271 // Verify the call is relevant for the plugin.
13272 if ( $this->_module_id != fs_request_get( 'module_id' ) ) {
13273 return false;
13274 }
13275
13276 // Verify it's one of the specified actions.
13277 if ( is_string( $actions ) ) {
13278 $actions = explode( ',', $actions );
13279 }
13280
13281 if ( is_array( $actions ) && 0 < count( $actions ) ) {
13282 $ajax_action = fs_request_get( 'action' );
13283
13284 foreach ( $actions as $action ) {
13285 if ( $ajax_action === $this->get_action_tag( $action ) ) {
13286 return true;
13287 }
13288 }
13289 }
13290
13291 return false;
13292 }
13293
13294 /**
13295 * Check if it's an AJAX call targeted for current request.
13296 *
13297 * @author Vova Feldman (@svovaf)
13298 * @since 1.2.0
13299 *
13300 * @param array|string $actions Collection of AJAX actions.
13301 * @param number|null $module_id
13302 *
13303 * @return bool
13304 */
13305 static function is_ajax_action_static( $actions, $module_id = null ) {
13306 // Verify it's an ajax call.
13307 if ( ! self::is_ajax() ) {
13308 return false;
13309 }
13310
13311
13312 if ( ! empty( $module_id ) ) {
13313 // Verify the call is relevant for the plugin.
13314 if ( $module_id != fs_request_get( 'module_id' ) ) {
13315 return false;
13316 }
13317 }
13318
13319 // Verify it's one of the specified actions.
13320 if ( is_string( $actions ) ) {
13321 $actions = explode( ',', $actions );
13322 }
13323
13324 if ( is_array( $actions ) && 0 < count( $actions ) ) {
13325 $ajax_action = fs_request_get( 'action' );
13326
13327 foreach ( $actions as $action ) {
13328 if ( $ajax_action === self::get_ajax_action_static( $action, $module_id ) ) {
13329 return true;
13330 }
13331 }
13332 }
13333
13334 return false;
13335 }
13336
13337 /**
13338 * @author Vova Feldman (@svovaf)
13339 * @since 1.1.7
13340 *
13341 * @return bool
13342 */
13343 static function is_cron() {
13344 return ( defined( 'DOING_CRON' ) && DOING_CRON );
13345 }
13346
13347 /**
13348 * Check if a real user is visiting the admin dashboard.
13349 *
13350 * @author Vova Feldman (@svovaf)
13351 * @since 1.1.7
13352 *
13353 * @return bool
13354 */
13355 function is_user_in_admin() {
13356 return is_admin() && ! self::is_ajax() && ! self::is_cron();
13357 }
13358
13359 /**
13360 * Check if a real user is in the customizer view.
13361 *
13362 * @author Vova Feldman (@svovaf)
13363 * @since 1.2.2.7
13364 *
13365 * @return bool
13366 */
13367 static function is_customizer() {
13368 return is_customize_preview();
13369 }
13370
13371 /**
13372 * Check if running in HTTPS and if site's plan matching the specified plan.
13373 *
13374 * @param string $plan
13375 * @param bool $exact
13376 *
13377 * @return bool
13378 */
13379 function is_ssl_and_plan( $plan, $exact = false ) {
13380 return ( $this->is_ssl() && $this->is_plan( $plan, $exact ) );
13381 }
13382
13383 /**
13384 * Construct plugin's settings page URL.
13385 *
13386 * @author Vova Feldman (@svovaf)
13387 * @since 1.0.4
13388 *
13389 * @param string $page
13390 * @param array $params
13391 * @param bool|null $network
13392 *
13393 * @return string
13394 */
13395 function _get_admin_page_url( $page = '', $params = array(), $network = null ) {
13396 if ( is_null( $network ) ) {
13397 $network = (
13398 $this->_is_network_active &&
13399 ( fs_is_network_admin() || ! $this->is_delegated_connection() )
13400 );
13401 }
13402
13403 if ( 0 < count( $params ) ) {
13404 foreach ( $params as $k => $v ) {
13405 $params[ $k ] = urlencode( $v );
13406 }
13407 }
13408
13409 $page_param = $this->_menu->get_slug( $page );
13410
13411 if ( empty( $page ) &&
13412 $this->is_theme() &&
13413 // Show the opt-in as an overlay for free wp.org themes or themes without any settings page.
13414 ( $this->is_free_wp_org_theme() || ! $this->has_settings_menu() )
13415 ) {
13416 $params[ $this->get_unique_affix() . '_show_optin' ] = 'true';
13417
13418 return add_query_arg(
13419 $params,
13420 $this->admin_url( 'themes.php', 'admin', $network )
13421 );
13422 }
13423
13424 if ( ! $this->has_settings_menu() ) {
13425 if ( ! empty( $page ) ) {
13426 // Module doesn't have a setting page, but since the request is for
13427 // a specific Freemius page, use the admin.php path.
13428 return add_query_arg( array_merge( $params, array(
13429 'page' => $page_param,
13430 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
13431 } else {
13432 if ( $this->is_activation_mode() ) {
13433 /**
13434 * @author Vova Feldman
13435 * @since 1.2.1.6
13436 *
13437 * If plugin doesn't have a settings page, create one for the opt-in screen.
13438 */
13439 return add_query_arg( array_merge( $params, array(
13440 'page' => $this->_slug,
13441 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
13442 } else {
13443 // Plugin without a settings page.
13444 return add_query_arg(
13445 $params,
13446 $this->admin_url( 'plugins.php', 'admin', $network )
13447 );
13448 }
13449 }
13450 }
13451
13452 // Module has a submenu settings page.
13453 if ( ! $this->_menu->is_top_level() ) {
13454 $parent_slug = $this->_menu->get_parent_slug();
13455 $menu_file = ( false !== strpos( $parent_slug, '.php' ) ) ?
13456 $parent_slug :
13457 'admin.php';
13458
13459 return add_query_arg( array_merge( $params, array(
13460 'page' => $page_param,
13461 ) ), $this->admin_url( $menu_file, 'admin', $network ) );
13462 }
13463
13464 // Module has a top level CPT settings page.
13465 if ( $this->_menu->is_cpt() ) {
13466 if ( empty( $page ) && $this->is_activation_mode() ) {
13467 return add_query_arg( array_merge( $params, array(
13468 'page' => $page_param
13469 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
13470 } else {
13471 if ( ! empty( $page ) ) {
13472 $params['page'] = $page_param;
13473 }
13474
13475 return add_query_arg(
13476 $params,
13477 $this->admin_url( $this->_menu->get_raw_slug(), 'admin', $network )
13478 );
13479 }
13480 }
13481
13482 // Module has a custom top level settings page.
13483 return add_query_arg( array_merge( $params, array(
13484 'page' => $page_param,
13485 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
13486 }
13487
13488 #--------------------------------------------------------------------------------
13489 #region Multisite
13490 #--------------------------------------------------------------------------------
13491
13492 /**
13493 * @author Leo Fajardo (@leorw)
13494 * @since 2.0.0
13495 *
13496 * @return bool
13497 */
13498 function is_network_active() {
13499 return $this->_is_network_active;
13500 }
13501
13502 /**
13503 * Delegate activation for the given sites in the network (or all sites if `null`) to site admins.
13504 *
13505 * @author Leo Fajardo (@leorw)
13506 * @since 2.0.0
13507 *
13508 * @param array|null $sites
13509 */
13510 private function delegate_connection( $sites = null ) {
13511 $this->_logger->entrance();
13512
13513 $this->_admin_notices->remove_sticky( 'connect_account' );
13514
13515 if ( is_null( $sites ) ) {
13516 // All sites delegation.
13517 $this->_storage->store( 'is_delegated_connection', true, true, true );
13518 } else {
13519 // Specified sites delegation.
13520 foreach ( $sites as $site ) {
13521 $this->delegate_site_connection( $site['blog_id'] );
13522 }
13523 }
13524
13525 $this->network_upgrade_mode_completed();
13526 }
13527
13528 /**
13529 * Delegate specific network site conncetion to the site admin.
13530 *
13531 * @author Vova Feldman (@svovaf)
13532 * @since 2.0.0
13533 *
13534 * @param int $blog_id
13535 */
13536 private function delegate_site_connection( $blog_id ) {
13537 $this->_storage->store( 'is_delegated_connection', true, $blog_id, true );
13538 }
13539
13540 /**
13541 * Check if super-admin delegated the connection of ALL sites to the site admins.
13542 *
13543 * @author Vova Feldman (@svovaf)
13544 * @since 2.0.0
13545 *
13546 * @return bool
13547 */
13548 function is_network_delegated_connection() {
13549 if ( ! $this->_is_network_active ) {
13550 return false;
13551 }
13552
13553 return $this->_storage->get( 'is_delegated_connection', false, true );
13554 }
13555
13556 /**
13557 * @author Leo Fajardo (@leorw)
13558 * @since 2.0.0
13559 *
13560 * @param int $blog_id
13561 *
13562 * @return bool
13563 */
13564 function is_site_delegated_connection( $blog_id = 0 ) {
13565 if ( ! $this->_is_network_active ) {
13566 return false;
13567 }
13568
13569 if ( 0 == $blog_id ) {
13570 $blog_id = get_current_blog_id();
13571 }
13572
13573 return $this->_storage->get( 'is_delegated_connection', false, $blog_id );
13574 }
13575
13576 /**
13577 * Check if delegated the connection. When running within the the network admin,
13578 * and haven't specified the blog ID, checks if network level delegated. If running
13579 * within a site admin or specified a blog ID, check if delegated the connection for
13580 * the current context site.
13581 *
13582 * If executed outside the the admin, check if delegated the connection
13583 * for the current context site OR the whole network.
13584 *
13585 * @author Vova Feldman (@svovaf)
13586 * @since 2.0.0
13587 *
13588 * @param int $blog_id If set, checks if network delegated or blog specific delegated.
13589 *
13590 * @return bool
13591 */
13592 function is_delegated_connection( $blog_id = 0 ) {
13593 if ( ! $this->_is_network_active ) {
13594 return false;
13595 }
13596
13597 if ( fs_is_network_admin() && 0 == $blog_id ) {
13598 return $this->is_network_delegated_connection();
13599 }
13600
13601 return (
13602 $this->is_network_delegated_connection() ||
13603 $this->is_site_delegated_connection( $blog_id )
13604 );
13605 }
13606
13607 /**
13608 * Check if the current module is active for the site.
13609 *
13610 * @author Vova Feldman (@svovaf)
13611 * @since 2.0.0
13612 *
13613 * @param int $blog_id
13614 *
13615 * @return bool
13616 */
13617 function is_active_for_site( $blog_id ) {
13618 if ( ! is_multisite() ) {
13619 // Not a multisite and this code is executed, means that the plugin is active.
13620 return true;
13621 }
13622
13623 if ( $this->is_theme() ) {
13624 // All themes are site level activated.
13625 return true;
13626 }
13627
13628 if ( $this->_is_network_active ) {
13629 // Plugin was network activated so it's active.
13630 return true;
13631 }
13632
13633 return in_array( $this->_plugin_basename, (array) get_blog_option( $blog_id, 'active_plugins', array() ) );
13634 }
13635
13636 /**
13637 * @author Leo Fajardo (@leorw)
13638 * @since 2.0.0
13639 *
13640 * @return array Active & public sites collection.
13641 */
13642 static function get_sites() {
13643 if ( ! is_multisite() ) {
13644 return array();
13645 }
13646
13647 /**
13648 * For consistency with get_blog_list() which only return active public sites.
13649 *
13650 * @author Vova Feldman (@svovaf)
13651 */
13652 $args = array(
13653 /**
13654 * Commented out in order to handle the migration of site options whether the site is public or not.
13655 *
13656 * @author Leo Fajardo (@leorw)
13657 * @since 2.2.1
13658 */
13659 // 'public' => 1,
13660 'archived' => 0,
13661 'mature' => 0,
13662 'spam' => 0,
13663 'deleted' => 0,
13664 );
13665
13666 if ( function_exists( 'get_sites' ) ) {
13667 // For WP 4.6 and above.
13668 return get_sites( $args );
13669 } else if ( function_exists( 'wp_get_sites' ) ) {
13670 // For WP 3.7 to WP 4.5.
13671 return wp_get_sites( $args );
13672 } else {
13673 // For WP 3.6 and below.
13674 return get_blog_list( 0, 'all' );
13675 }
13676 }
13677
13678 /**
13679 * Checks if a given blog is active.
13680 *
13681 * @author Vova Feldman (@svovaf)
13682 * @since 2.0.0
13683 *
13684 * @param $blog_id
13685 *
13686 * @return bool
13687 */
13688 private static function is_site_active( $blog_id ) {
13689 global $wpdb;
13690
13691 $blog_info = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->blogs} WHERE blog_id = %d", $blog_id ) );
13692
13693 if ( ! is_object( $blog_info ) ) {
13694 return false;
13695 }
13696
13697 return (
13698 true == $blog_info->public &&
13699 false == $blog_info->archived &&
13700 false == $blog_info->mature &&
13701 false == $blog_info->spam &&
13702 false == $blog_info->deleted
13703 );
13704 }
13705
13706 /**
13707 * Get a mapping between the site addresses to their blog IDs.
13708 *
13709 * @author Vova Feldman (@svovaf)
13710 * @since 2.0.0
13711 *
13712 * @return array {
13713 * @key string Site address without protocol with a trailing slash.
13714 * @value int Site's blog ID.
13715 * }
13716 */
13717 private function get_address_to_blog_map() {
13718 $sites = self::get_sites();
13719
13720 // Map site addresses to their blog IDs.
13721 $address_to_blog_map = array();
13722 foreach ( $sites as $site ) {
13723 $blog_id = self::get_site_blog_id( $site );
13724 $address = trailingslashit( fs_strip_url_protocol( get_site_url( $blog_id ) ) );
13725 $address_to_blog_map[ $address ] = $blog_id;
13726 }
13727
13728 return $address_to_blog_map;
13729 }
13730
13731 /**
13732 * Get a mapping between the site addresses to their blog IDs.
13733 *
13734 * @author Vova Feldman (@svovaf)
13735 * @since 2.0.0
13736 *
13737 * @return array {
13738 * @key int Site's blog ID.
13739 * @value FS_Site Associated install.
13740 * }
13741 */
13742 function get_blog_install_map() {
13743 $sites = self::get_sites();
13744
13745 // Map site blog ID to its install.
13746 $install_map = array();
13747
13748 foreach ( $sites as $site ) {
13749 $blog_id = self::get_site_blog_id( $site );
13750 $install = $this->get_install_by_blog_id( $blog_id );
13751
13752 if ( is_object( $install ) ) {
13753 $install_map[ $blog_id ] = $install;
13754 }
13755 }
13756
13757 return $install_map;
13758 }
13759
13760 /**
13761 * Gets a map of module IDs that the given user has opted-in to.
13762 *
13763 * @author Leo Fajardo (@leorw)
13764 * @since 2.1.0
13765 *
13766 * @param number $fs_user_id
13767 *
13768 * @return array {
13769 * @key number $plugin_id
13770 * @value bool Always true.
13771 * }
13772 */
13773 private static function get_user_opted_in_module_ids_map( $fs_user_id ) {
13774 self::$_static_logger->entrance();
13775
13776 if ( ! is_multisite() ) {
13777 $installs = array_merge(
13778 self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN ),
13779 self::get_all_sites( WP_FS__MODULE_TYPE_THEME )
13780 );
13781 } else {
13782 $sites = self::get_sites();
13783
13784 $installs = array();
13785 foreach ( $sites as $site ) {
13786 $blog_id = self::get_site_blog_id( $site );
13787
13788 $installs = array_merge(
13789 $installs,
13790 self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN, $blog_id ),
13791 self::get_all_sites( WP_FS__MODULE_TYPE_THEME, $blog_id )
13792 );
13793 }
13794 }
13795
13796 $module_ids_map = array();
13797 foreach ( $installs as $install ) {
13798 if ( is_object( $install ) &&
13799 FS_Site::is_valid_id( $install->id ) &&
13800 FS_User::is_valid_id( $install->user_id ) &&
13801 ( $install->user_id == $fs_user_id )
13802 ) {
13803 $module_ids_map[ $install->plugin_id ] = true;
13804 }
13805 }
13806
13807 return $module_ids_map;
13808 }
13809
13810 /**
13811 * @author Leo Fajardo (@leorw)
13812 *
13813 * @return null|array {
13814 * 'install' => FS_Site Module's install,
13815 * 'blog_id' => string The associated blog ID.
13816 * }
13817 */
13818 function find_first_install() {
13819 $sites = self::get_sites();
13820
13821 foreach ( $sites as $site ) {
13822 $blog_id = self::get_site_blog_id( $site );
13823 $install = $this->get_install_by_blog_id( $blog_id );
13824
13825 if ( is_object( $install ) ) {
13826 return array(
13827 'install' => $install,
13828 'blog_id' => $blog_id
13829 );
13830 }
13831 }
13832
13833 return null;
13834 }
13835
13836 /**
13837 * Switches the Freemius site level context to a specified blog.
13838 *
13839 * @author Vova Feldman (@svovaf)
13840 * @since 2.0.0
13841 *
13842 * @param int $blog_id
13843 * @param FS_Site $install
13844 */
13845 function switch_to_blog( $blog_id, FS_Site $install = null ) {
13846 if ( $blog_id == $this->_context_is_network_or_blog_id ) {
13847 return;
13848 }
13849
13850 switch_to_blog( $blog_id );
13851 $this->_context_is_network_or_blog_id = $blog_id;
13852
13853 self::$_accounts->set_site_blog_context( $blog_id );
13854 $this->_storage->set_site_blog_context( $blog_id );
13855 $this->_storage->set_network_active( $this->_is_network_active, $this->is_delegated_connection( $blog_id ) );
13856
13857 $this->_site = is_object( $install ) ?
13858 $install :
13859 $this->get_install_by_blog_id( $blog_id );
13860
13861 $this->_user = false;
13862 $this->_licenses = false;
13863 $this->_license = null;
13864
13865 if ( is_object( $this->_site ) ) {
13866 // Try to fetch user from install.
13867 $this->_user = self::_get_user_by_id( $this->_site->user_id );
13868
13869 if ( ! is_object( $this->_user ) &&
13870 FS_User::is_valid_id( $this->_storage->prev_user_id )
13871 ) {
13872 // Try to fetch previously saved user.
13873 $this->_user = self::_get_user_by_id( $this->_storage->prev_user_id );
13874
13875 if ( ! is_object( $this->_user ) ) {
13876 // Fallback to network's user.
13877 $this->_user = $this->get_network_user();
13878 }
13879 }
13880
13881 $all_plugin_licenses = self::get_all_licenses( $this->_module_id );
13882
13883 if ( ! empty( $all_plugin_licenses ) ) {
13884 if ( ! FS_Plugin_License::is_valid_id( $this->_site->license_id ) ) {
13885 $this->_license = null;
13886 } else {
13887 $license_found = false;
13888 foreach ( $all_plugin_licenses as $license ) {
13889 if ( $license->id == $this->_site->license_id ) {
13890 // License found.
13891 $this->_license = $license;
13892 $license_found = true;
13893 break;
13894 }
13895 }
13896
13897 if ( $license_found ) {
13898 $this->link_license_2_user( $this->_license->id, $this->_user->id );
13899 }
13900 }
13901
13902 $this->_licenses = $this->get_user_licenses( $this->_user->id );
13903 }
13904 }
13905
13906 unset( $this->_site_api );
13907 unset( $this->_user_api );
13908 }
13909
13910 /**
13911 * Restore the blog context to the blog that originally loaded the module.
13912 *
13913 * @author Vova Feldman (@svovaf)
13914 * @since 2.0.0
13915 */
13916 function restore_current_blog() {
13917 $this->switch_to_blog( $this->_blog_id );
13918 }
13919
13920 /**
13921 * @author Vova Feldman (@svovaf)
13922 * @since 2.0.0
13923 *
13924 * @param array|WP_Site $site
13925 *
13926 * @return int
13927 */
13928 static function get_site_blog_id( &$site ) {
13929 return ( $site instanceof WP_Site ) ?
13930 $site->blog_id :
13931 ( is_object( $site ) && isset( $site->userblog_id ) ?
13932 $site->userblog_id :
13933 $site['blog_id'] );
13934 }
13935
13936 /**
13937 * @author Leo Fajardo (@leorw)
13938 * @since 2.0.0
13939 *
13940 * @param array|WP_Site|null $site
13941 *
13942 * @return array
13943 */
13944 function get_site_info( $site = null ) {
13945 $this->_logger->entrance();
13946
13947 $switched = false;
13948
13949 if ( is_null( $site ) ) {
13950 $url = get_site_url();
13951 $name = get_bloginfo( 'name' );
13952 $blog_id = null;
13953 } else {
13954 $blog_id = self::get_site_blog_id( $site );
13955
13956 if ( get_current_blog_id() != $blog_id ) {
13957 switch_to_blog( $blog_id );
13958 $switched = true;
13959 }
13960
13961 if ( $site instanceof WP_Site ) {
13962 $url = $site->siteurl;
13963 $name = $site->blogname;
13964 } else {
13965 $url = get_site_url( $blog_id );
13966 $name = get_bloginfo( 'name' );
13967 }
13968 }
13969
13970 $info = array(
13971 'uid' => $this->get_anonymous_id( $blog_id ),
13972 'url' => $url,
13973 'title' => $name,
13974 'language' => get_bloginfo( 'language' ),
13975 'charset' => get_bloginfo( 'charset' ),
13976 );
13977
13978 if ( is_numeric( $blog_id ) ) {
13979 $info['blog_id'] = $blog_id;
13980 }
13981
13982 if ( $switched ) {
13983 restore_current_blog();
13984 }
13985
13986 return $info;
13987 }
13988
13989 /**
13990 * Load the module's install based on the blog ID.
13991 *
13992 * @author Vova Feldman (@svovaf)
13993 * @since 2.0.0
13994 *
13995 * @param int|null $blog_id
13996 *
13997 * @return FS_Site
13998 */
13999 function get_install_by_blog_id( $blog_id = null ) {
14000 $installs = self::get_all_sites( $this->_module_type, $blog_id );
14001 $install = isset( $installs[ $this->_slug ] ) ? $installs[ $this->_slug ] : null;
14002
14003 if ( is_object( $install ) &&
14004 is_numeric( $install->id ) &&
14005 is_numeric( $install->user_id ) &&
14006 FS_Plugin_Plan::is_valid_id( $install->plan_id )
14007 ) {
14008 // Load site.
14009 $install = clone $install;
14010 }
14011
14012 return $install;
14013 }
14014
14015 /**
14016 * Check if module is installed on a specified site.
14017 *
14018 * @author Vova Feldman (@svovaf)
14019 * @since 2.0.0
14020 *
14021 * @param int|null $blog_id
14022 *
14023 * @return bool
14024 */
14025 function is_installed_on_site( $blog_id = null ) {
14026 $installs = self::get_all_sites( $this->_module_type, $blog_id );
14027 $install = isset( $installs[ $this->_slug ] ) ? $installs[ $this->_slug ] : null;
14028
14029 return (
14030 is_object( $install ) &&
14031 is_numeric( $install->id ) &&
14032 is_numeric( $install->user_id ) &&
14033 FS_Plugin_Plan::is_valid_id( $install->plan_id )
14034 );
14035 }
14036
14037 /**
14038 * Check if super-admin connected at least one site via the network opt-in.
14039 *
14040 * @author Vova Feldman (@svovaf)
14041 * @since 2.0.0
14042 *
14043 * @return bool
14044 */
14045 function is_network_registered() {
14046 if ( ! $this->_is_network_active ) {
14047 return false;
14048 }
14049
14050 return FS_User::is_valid_id( $this->_storage->network_user_id );
14051 }
14052
14053 /**
14054 * Returns the main user associated with the network.
14055 *
14056 * @author Vova Feldman (@svovaf)
14057 * @since 2.0.0
14058 *
14059 * @return FS_User
14060 */
14061 function get_network_user() {
14062 if ( ! $this->_is_network_active ) {
14063 return null;
14064 }
14065
14066 return FS_User::is_valid_id( $this->_storage->network_user_id ) ?
14067 self::_get_user_by_id( $this->_storage->network_user_id ) :
14068 null;
14069 }
14070
14071 /**
14072 * Returns the current context user or the network's main user.
14073 *
14074 * @author Vova Feldman (@svovaf)
14075 * @since 2.0.0
14076 *
14077 * @return FS_User
14078 */
14079 function get_current_or_network_user() {
14080 return ( $this->_user instanceof FS_User ) ?
14081 $this->_user :
14082 $this->get_network_user();
14083 }
14084
14085 /**
14086 * Returns the main install associated with the network.
14087 *
14088 * @author Vova Feldman (@svovaf)
14089 * @since 2.0.0
14090 *
14091 * @return FS_Site
14092 */
14093 function get_network_install() {
14094 if ( ! $this->_is_network_active ) {
14095 return null;
14096 }
14097
14098 return FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ?
14099 $this->get_install_by_blog_id( $this->_storage->network_install_blog_id ) :
14100 null;
14101 }
14102
14103 /**
14104 * Returns the blog ID that is associated with the main install.
14105 *
14106 * @author Leo Fajardo (@leorw)
14107 * @since 2.0.0
14108 *
14109 * @return int|null
14110 */
14111 function get_network_install_blog_id() {
14112 if ( ! $this->_is_network_active ) {
14113 return null;
14114 }
14115
14116 return FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ?
14117 $this->_storage->network_install_blog_id :
14118 null;
14119 }
14120
14121 /**
14122 * Returns the current context install or the network's main install.
14123 *
14124 * @author Vova Feldman (@svovaf)
14125 * @since 2.0.0
14126 *
14127 * @return FS_Site
14128 */
14129 function get_current_or_network_install() {
14130 return ( $this->_site instanceof FS_Site ) ?
14131 $this->_site :
14132 $this->get_network_install();
14133 }
14134
14135 /**
14136 * Check if executing a site level action from the network level admin.
14137 *
14138 * @author Vova Feldman (@svovaf)
14139 * @since 2.0.0
14140 *
14141 * @return false|int If yes, return the requested blog ID.
14142 */
14143 private function is_network_level_site_specific_action() {
14144 if ( ! $this->_is_network_active ) {
14145 return false;
14146 }
14147
14148 if ( ! fs_is_network_admin() ) {
14149 return false;
14150 }
14151
14152 $blog_id = fs_request_get( 'blog_id', '' );
14153
14154 return is_numeric( $blog_id ) ? $blog_id : false;
14155 }
14156
14157 /**
14158 * Check if executing an action from the network level admin.
14159 *
14160 * @author Vova Feldman (@svovaf)
14161 * @since 2.0.0
14162 *
14163 * @return bool
14164 */
14165 private function is_network_level_action() {
14166 return ( $this->_is_network_active && fs_is_network_admin() );
14167 }
14168
14169 /**
14170 * Needs to be executed after site deactivation, archive, deletion, or flag as spam.
14171 * 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.
14172 *
14173 * @author Vova Feldman (@svovaf)
14174 * @since 2.0.0
14175 *
14176 * @param int $context_blog_id
14177 */
14178 private function update_multisite_data_after_site_deactivation( $context_blog_id = 0 ) {
14179 $this->_logger->entrance();
14180
14181 if ( $this->_is_network_active ) {
14182 if ( $context_blog_id == $this->_storage->network_install_blog_id ) {
14183 $installs_map = $this->get_blog_install_map();
14184
14185 foreach ( $installs_map as $blog_id => $install ) {
14186 /**
14187 * @var FS_Site $install
14188 */
14189 if ( $context_blog_id == $blog_id ) {
14190 continue;
14191 }
14192
14193 if ( $install->user_id != $this->_storage->network_user_id ) {
14194 continue;
14195 }
14196
14197 // Switch reference to a blog that is opted-in and belong to the same super-admin.
14198 $this->_storage->network_install_blog_id = $blog_id;
14199 break;
14200 }
14201 }
14202 }
14203
14204 if ( $this->is_sync_cron_scheduled() &&
14205 $context_blog_id == $this->get_sync_cron_blog_id()
14206 ) {
14207 $this->schedule_sync_cron( WP_FS__SCRIPT_START_TIME, true, $context_blog_id );
14208 }
14209
14210 if ( $this->is_install_sync_scheduled() &&
14211 $context_blog_id == $this->get_install_sync_cron_blog_id()
14212 ) {
14213 $this->schedule_install_sync( $context_blog_id );
14214 }
14215 }
14216
14217 /**
14218 * Executed after site deactivation, archive, or flag as spam.
14219 *
14220 * @author Vova Feldman (@svovaf)
14221 * @since 2.0.0
14222 *
14223 * @param int $context_blog_id
14224 */
14225 public function _after_site_deactivated_callback( $context_blog_id = 0 ) {
14226 $this->_logger->entrance();
14227
14228 $install = $this->get_install_by_blog_id( $context_blog_id );
14229
14230 if ( ! is_object( $install ) ) {
14231 // Site not connected.
14232 return;
14233 }
14234
14235 $this->update_multisite_data_after_site_deactivation( $context_blog_id );
14236
14237 $current_blog_id = get_current_blog_id();
14238
14239 $this->switch_to_blog( $context_blog_id );
14240
14241 // Send deactivation event.
14242 $this->sync_install( array(
14243 'is_active' => false,
14244 ) );
14245
14246 $this->switch_to_blog( $current_blog_id );
14247 }
14248
14249 /**
14250 * Executed after site deletion.
14251 *
14252 * @author Vova Feldman (@svovaf)
14253 * @since 2.0.0
14254 *
14255 * @param int $context_blog_id
14256 * @param bool $drop True if site's database tables should be dropped. Default is false.
14257 */
14258 public function _after_site_deleted_callback( $context_blog_id = 0, $drop = false ) {
14259 $this->_logger->entrance();
14260
14261 $install = $this->get_install_by_blog_id( $context_blog_id );
14262
14263 if ( ! is_object( $install ) ) {
14264 // Site not connected.
14265 return;
14266 }
14267
14268 $this->update_multisite_data_after_site_deactivation( $context_blog_id );
14269
14270 $current_blog_id = get_current_blog_id();
14271
14272 $this->switch_to_blog( $context_blog_id );
14273
14274 if ( $drop ) {
14275 // Delete install if dropping site DB.
14276 $this->delete_account_event();
14277 } else {
14278 // Send deactivation event.
14279 $this->sync_install( array(
14280 'is_active' => false,
14281 ) );
14282 }
14283
14284 $this->switch_to_blog( $current_blog_id );
14285 }
14286
14287 /**
14288 * Executed after site re-activation.
14289 *
14290 * @author Vova Feldman (@svovaf)
14291 * @since 2.0.0
14292 *
14293 * @param int $context_blog_id
14294 */
14295 public function _after_site_reactivated_callback( $context_blog_id = 0 ) {
14296 $this->_logger->entrance();
14297
14298 $install = $this->get_install_by_blog_id( $context_blog_id );
14299
14300 if ( ! is_object( $install ) ) {
14301 // Site not connected.
14302 return;
14303 }
14304
14305 if ( ! self::is_site_active( $context_blog_id ) ) {
14306 // Site not yet active (can be in spam mode, archived, deleted...).
14307 return;
14308 }
14309
14310 $current_blog_id = get_current_blog_id();
14311
14312 $this->switch_to_blog( $context_blog_id );
14313
14314 // Send re-activation event.
14315 $this->sync_install( array(
14316 'is_active' => true,
14317 ) );
14318
14319 $this->switch_to_blog( $current_blog_id );
14320 }
14321
14322 #endregion Multisite
14323
14324 /**
14325 * @author Leo Fajardo (@leorw)
14326 *
14327 * @param string $path
14328 * @param string $scheme
14329 * @param bool $network
14330 *
14331 * @return string
14332 */
14333 private function admin_url( $path = '', $scheme = 'admin', $network = true ) {
14334 return ( $this->_is_network_active && $network ) ?
14335 network_admin_url( $path, $scheme ) :
14336 admin_url( $path, $scheme );
14337 }
14338
14339 /**
14340 * Check if currently in a specified admin page.
14341 *
14342 * @author Vova Feldman (@svovaf)
14343 * @since 1.2.2.7
14344 *
14345 * @param string $page
14346 *
14347 * @return bool
14348 */
14349 function is_admin_page( $page ) {
14350 return ( $this->_menu->get_slug( $page ) === fs_request_get( 'page', '', 'get' ) );
14351 }
14352
14353 /**
14354 * Get module's main admin setting page URL.
14355 *
14356 * @author Vova Feldman (@svovaf)
14357 * @since 1.2.2.7
14358 *
14359 * @return string
14360 */
14361 function main_menu_url() {
14362 return $this->_menu->main_menu_url();
14363 }
14364
14365 /**
14366 * Check if currently on the theme's setting page or
14367 * on any of the Freemius added pages (via tabs).
14368 *
14369 * @author Vova Feldman (@svovaf)
14370 * @since 1.2.2.7
14371 *
14372 * @return bool
14373 */
14374 function is_theme_settings_page() {
14375 return fs_starts_with(
14376 fs_request_get( 'page', '', 'get' ),
14377 $this->_menu->get_slug()
14378 );
14379 }
14380
14381 /**
14382 * Plugin's account page + sync license URL.
14383 *
14384 * @author Vova Feldman (@svovaf)
14385 * @since 1.1.9.1
14386 *
14387 * @param bool|number $plugin_id
14388 * @param bool $add_action_nonce
14389 * @param array $params
14390 *
14391 * @return string
14392 */
14393 function _get_sync_license_url( $plugin_id = false, $add_action_nonce = true, $params = array() ) {
14394 if ( is_numeric( $plugin_id ) ) {
14395 $params['plugin_id'] = $plugin_id;
14396 }
14397
14398 return $this->get_account_url(
14399 $this->get_unique_affix() . '_sync_license',
14400 $params,
14401 $add_action_nonce
14402 );
14403 }
14404
14405 /**
14406 * Plugin's account URL.
14407 *
14408 * @author Vova Feldman (@svovaf)
14409 * @since 1.0.4
14410 *
14411 * @param bool|string $action
14412 * @param array $params
14413 *
14414 * @param bool $add_action_nonce
14415 *
14416 * @return string
14417 */
14418 function get_account_url( $action = false, $params = array(), $add_action_nonce = true ) {
14419 if ( is_string( $action ) ) {
14420 $params['fs_action'] = $action;
14421 }
14422
14423 self::require_pluggable_essentials();
14424
14425 return ( $add_action_nonce && is_string( $action ) ) ?
14426 fs_nonce_url( $this->_get_admin_page_url( 'account', $params ), $action ) :
14427 $this->_get_admin_page_url( 'account', $params );
14428 }
14429
14430 /**
14431 * @author Vova Feldman (@svovaf)
14432 * @since 1.2.0
14433 *
14434 * @param string $tab
14435 * @param bool $action
14436 * @param array $params
14437 * @param bool $add_action_nonce
14438 *
14439 * @return string
14440 *
14441 * @uses get_account_url()
14442 */
14443 function get_account_tab_url( $tab, $action = false, $params = array(), $add_action_nonce = true ) {
14444 $params['tab'] = $tab;
14445
14446 return $this->get_account_url( $action, $params, $add_action_nonce );
14447 }
14448
14449 /**
14450 * Plugin's account URL.
14451 *
14452 * @author Vova Feldman (@svovaf)
14453 * @since 1.0.4
14454 *
14455 * @param bool|string $topic
14456 * @param bool|string $message
14457 *
14458 * @return string
14459 */
14460 function contact_url( $topic = false, $message = false ) {
14461 $params = array();
14462 if ( is_string( $topic ) ) {
14463 $params['topic'] = $topic;
14464 }
14465 if ( is_string( $message ) ) {
14466 $params['message'] = $message;
14467 }
14468
14469 if ( $this->is_addon() ) {
14470 $params['addon_id'] = $this->get_id();
14471
14472 return $this->get_parent_instance()->_get_admin_page_url( 'contact', $params );
14473 } else {
14474 return $this->_get_admin_page_url( 'contact', $params );
14475 }
14476 }
14477
14478 /**
14479 * Add-on direct info URL.
14480 *
14481 * @author Vova Feldman (@svovaf)
14482 * @since 1.1.0
14483 *
14484 * @param string $slug
14485 *
14486 * @return string
14487 */
14488 function addon_url( $slug ) {
14489 return $this->_get_admin_page_url( 'addons', array(
14490 'slug' => $slug
14491 ) );
14492 }
14493
14494 /**
14495 * Add-ons URL.
14496 *
14497 * @author Vova Feldman (@svovaf)
14498 * @since 2.4.5
14499 *
14500 * @return string
14501 */
14502 function get_addons_url() {
14503 return $this->_get_admin_page_url( 'addons' );
14504 }
14505
14506 /* Logger
14507 ------------------------------------------------------------------------------------------------------------------*/
14508 /**
14509 * @param string $id
14510 * @param bool $prefix_slug
14511 *
14512 * @return FS_Logger
14513 */
14514 function get_logger( $id = '', $prefix_slug = true ) {
14515 return FS_Logger::get_logger( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id );
14516 }
14517
14518 /**
14519 * Note: This method is used externally so don't delete it.
14520 *
14521 * @param $id
14522 * @param bool $load_options
14523 * @param bool $prefix_slug
14524 *
14525 * @return FS_Option_Manager
14526 */
14527 function get_options_manager( $id, $load_options = false, $prefix_slug = true ) {
14528 return FS_Option_Manager::get_manager( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id, $load_options );
14529 }
14530
14531 /* Security
14532 ------------------------------------------------------------------------------------------------------------------*/
14533 private static function _encrypt( $str ) {
14534 if ( is_null( $str ) ) {
14535 return null;
14536 }
14537
14538 /**
14539 * The encrypt/decrypt functions are used to protect
14540 * the user from messing up with some of the sensitive
14541 * data stored for the module as a JSON in the database.
14542 *
14543 * I used the same suggested hack by the theme review team.
14544 * For more details, look at the function `Base64UrlDecode()`
14545 * in `./sdk/FreemiusBase.php`.
14546 *
14547 * @todo Remove this hack once the base64 error is removed from the Theme Check.
14548 *
14549 * @author Vova Feldman (@svovaf)
14550 * @since 1.2.2
14551 */
14552 $fn = 'base64' . '_encode';
14553
14554 return $fn( $str );
14555 }
14556
14557 static function _decrypt( $str ) {
14558 if ( is_null( $str ) ) {
14559 return null;
14560 }
14561
14562 /**
14563 * The encrypt/decrypt functions are used to protect
14564 * the user from messing up with some of the sensitive
14565 * data stored for the module as a JSON in the database.
14566 *
14567 * I used the same suggested hack by the theme review team.
14568 * For more details, look at the function `Base64UrlDecode()`
14569 * in `./sdk/FreemiusBase.php`.
14570 *
14571 * @todo Remove this hack once the base64 error is removed from the Theme Check.
14572 *
14573 * @author Vova Feldman (@svovaf)
14574 * @since 1.2.2
14575 */
14576 $fn = 'base64' . '_decode';
14577
14578 return $fn( $str );
14579 }
14580
14581 /**
14582 * @author Vova Feldman (@svovaf)
14583 * @since 1.0.5
14584 *
14585 * @param FS_Entity $entity
14586 *
14587 * @return FS_Entity Return an encrypted clone entity.
14588 */
14589 private static function _encrypt_entity( FS_Entity $entity ) {
14590 $clone = clone $entity;
14591 $props = get_object_vars( $entity );
14592
14593 foreach ( $props as $key => $val ) {
14594 $clone->{$key} = self::_encrypt( $val );
14595 }
14596
14597 return $clone;
14598 }
14599
14600 /**
14601 * @author Vova Feldman (@svovaf)
14602 * @since 1.0.5
14603 *
14604 * @param FS_Entity $entity
14605 *
14606 * @return FS_Entity Return an decrypted clone entity.
14607 */
14608 private static function decrypt_entity( FS_Entity $entity ) {
14609 $clone = clone $entity;
14610 $props = get_object_vars( $entity );
14611
14612 foreach ( $props as $key => $val ) {
14613 $clone->{$key} = self::_decrypt( $val );
14614 }
14615
14616 return $clone;
14617 }
14618
14619 /**
14620 * Tries to activate account based on POST params.
14621 *
14622 * @author Vova Feldman (@svovaf)
14623 * @since 1.0.2
14624 *
14625 * @deprecated Not in use, outdated.
14626 */
14627 function _activate_account() {
14628 if ( $this->is_registered() ) {
14629 // Already activated.
14630 return;
14631 }
14632
14633 self::_clean_admin_content_section();
14634
14635 if ( fs_request_is_action( 'activate' ) && fs_request_is_post() ) {
14636 // check_admin_referer( 'activate_' . $this->_plugin->public_key );
14637
14638 // Verify matching plugin details.
14639 if ( $this->_plugin->id != fs_request_get( 'plugin_id' ) || $this->_slug != fs_request_get( 'plugin_slug' ) ) {
14640 return;
14641 }
14642
14643 $user = new FS_User();
14644 $user->id = fs_request_get( 'user_id' );
14645 $user->public_key = fs_request_get( 'user_public_key' );
14646 $user->secret_key = fs_request_get( 'user_secret_key' );
14647 $user->email = fs_request_get( 'user_email' );
14648 $user->first = fs_request_get( 'user_first' );
14649 $user->last = fs_request_get( 'user_last' );
14650 $user->is_verified = fs_request_get_bool( 'user_is_verified' );
14651
14652 $site = new FS_Site();
14653 $site->id = fs_request_get( 'install_id' );
14654 $site->public_key = fs_request_get( 'install_public_key' );
14655 $site->secret_key = fs_request_get( 'install_secret_key' );
14656 $site->plan_id = fs_request_get( 'plan_id' );
14657
14658 $plans = array();
14659 $plans_data = json_decode( urldecode( fs_request_get( 'plans' ) ) );
14660 foreach ( $plans_data as $p ) {
14661 $plan = new FS_Plugin_Plan( $p );
14662 if ( $site->plan_id == $plan->id ) {
14663 $plan->title = fs_request_get( 'plan_title' );
14664 $plan->name = fs_request_get( 'plan_name' );
14665 }
14666
14667 $plans[] = $plan;
14668 }
14669
14670 $this->_set_account( $user, $site, $plans );
14671
14672 // Reload the page with the keys.
14673 fs_redirect( $this->_get_admin_page_url() );
14674 }
14675 }
14676
14677 /**
14678 * @author Vova Feldman (@svovaf)
14679 * @since 1.0.7
14680 *
14681 * @param string $email
14682 *
14683 * @return FS_User|false
14684 */
14685 static function _get_user_by_email( $email ) {
14686 self::$_static_logger->entrance();
14687
14688 $email = trim( strtolower( $email ) );
14689
14690 $users = self::get_all_users();
14691
14692 if ( is_array( $users ) ) {
14693 foreach ( $users as $user ) {
14694 if ( $email === trim( strtolower( $user->email ) ) ) {
14695 return $user;
14696 }
14697 }
14698 }
14699
14700 return false;
14701 }
14702
14703 #----------------------------------------------------------------------------------
14704 #region Account (Loading, Updates & Activation)
14705 #----------------------------------------------------------------------------------
14706
14707 /***
14708 * Load account information (user + site).
14709 *
14710 * @author Vova Feldman (@svovaf)
14711 * @since 1.0.1
14712 */
14713 private function _load_account() {
14714 $this->_logger->entrance();
14715
14716 $this->do_action( 'before_account_load' );
14717
14718 $users = self::get_all_users();
14719 $plans = self::get_all_plans( $this->_module_type );
14720
14721 if ( $this->_logger->is_on() && is_admin() ) {
14722 $this->_logger->log( 'users = ' . var_export( $users, true ) );
14723 $this->_logger->log( 'plans = ' . var_export( $plans, true ) );
14724 }
14725
14726 $site = fs_is_network_admin() ?
14727 $this->get_network_install() :
14728 $this->get_install_by_blog_id();
14729
14730 if ( fs_is_network_admin() &&
14731 $this->is_network_active() &&
14732 ! is_object( $site ) &&
14733 FS_Site::is_valid_id( $this->_storage->network_install_blog_id )
14734 ) {
14735 $first_install = $this->find_first_install();
14736
14737 if ( is_null( $first_install ) ) {
14738 unset( $this->_storage->network_install_blog_id );
14739 } else {
14740 $site = $first_install['install'];
14741 $this->_storage->network_install_blog_id = $first_install['blog_id'];
14742 }
14743 }
14744
14745 if ( is_object( $site ) &&
14746 is_numeric( $site->id ) &&
14747 is_numeric( $site->user_id ) &&
14748 FS_Plugin_Plan::is_valid_id( $site->plan_id )
14749 ) {
14750 // Load site.
14751 $this->_site = $site;
14752
14753 // Load plans.
14754 $this->_plans = $plans[ $this->_slug ];
14755 if ( ! is_array( $this->_plans ) || empty( $this->_plans ) ) {
14756 $this->_sync_plans();
14757 } else {
14758 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
14759 if ( $this->_plans[ $i ] instanceof FS_Plugin_Plan ) {
14760 $this->_plans[ $i ] = self::decrypt_entity( $this->_plans[ $i ] );
14761 } else {
14762 unset( $this->_plans[ $i ] );
14763 }
14764 }
14765 }
14766 }
14767
14768 $user = null;
14769 if ( fs_is_network_admin() && $this->_is_network_active ) {
14770 $user = $this->get_network_user();
14771 }
14772
14773 if ( is_object( $user ) ) {
14774 $this->_user = clone $user;
14775 } else if ( $this->_site ) {
14776 $user = self::_get_user_by_id( $this->_site->user_id );
14777
14778 if ( ! is_object( $user ) && FS_User::is_valid_id( $this->_storage->prev_user_id ) ) {
14779 /**
14780 * Try to load the previous owner. This recovery is used for the following use-case:
14781 * 1. Opt-in
14782 * 2. Cloning site1 to site2
14783 * 3. Ownership switch in site1 (same applies for site2)
14784 * 4. Install data sync on site2
14785 * 5. Now site2's install is associated with the new owner which does not exists locally.
14786 */
14787 $user = self::_get_user_by_id( $this->_storage->prev_user_id );
14788 }
14789
14790 if ( ! is_object( $user ) ) {
14791 /**
14792 * This is a special fault tolerance mechanism to handle a scenario that the user data is missing.
14793 */
14794 $user = $this->fetch_user_by_install();
14795 }
14796
14797 $this->_user = ( $user instanceof FS_User ) ?
14798 clone $user :
14799 null;
14800 }
14801
14802 if ( is_object( $this->_user ) ) {
14803 // Load licenses.
14804 $this->_licenses = $this->get_user_licenses( $this->_user->id );
14805 }
14806
14807 if ( is_object( $this->_site ) ) {
14808 $this->_license = $this->_get_license_by_id( $this->_site->license_id );
14809
14810 if ( $this->_site->version != $this->get_plugin_version() ) {
14811 // If stored install version is different than current installed plugin version,
14812 // then update plugin version event.
14813 $this->update_plugin_version_event();
14814 }
14815 }
14816
14817 if ( true === $this->_storage->require_license_activation &&
14818 ! fs_request_get_bool( 'require_license', true )
14819 ) {
14820 $this->_storage->require_license_activation = false;
14821 }
14822
14823 if ( $this->is_theme() ) {
14824 $this->_register_account_hooks();
14825 }
14826 }
14827
14828 /**
14829 * Special user recovery mechanism.
14830 *
14831 * @author Vova Feldman (@svovaf)
14832 * @since 2.0.0
14833 *
14834 * @return \FS_User|mixed
14835 */
14836 private function fetch_user_by_install() {
14837 $api = $this->get_api_site_scope();
14838
14839 $uid = $this->get_anonymous_id();
14840 $request_path = "/users/{$this->_site->user_id}.json?uid={$uid}";
14841
14842 $result = $api->get( $request_path, false, WP_FS__TIME_10_MIN_IN_SEC );
14843
14844 if ( $this->is_api_result_entity( $result ) ) {
14845 $user = new FS_User( $result );
14846 $this->_user = $user;
14847 $this->_store_user();
14848
14849 return $user;
14850 }
14851
14852 $error_code = FS_Api::get_error_code( $result );
14853
14854 if ( in_array( $error_code, array( 'invalid_unique_id', 'user_cannot_be_recovered' ) ) ) {
14855 /**
14856 * Those API errors will continue coming and are not recoverable with the
14857 * current site's data. Therefore, extend the API call's cached result to 7 days.
14858 */
14859 $api->update_cache_expiration( $request_path, WP_FS__TIME_WEEK_IN_SEC );
14860 }
14861
14862 return $result;
14863 }
14864
14865 /**
14866 * @author Vova Feldman (@svovaf)
14867 * @since 1.0.1
14868 *
14869 * @param FS_User $user
14870 * @param FS_Site $site
14871 * @param bool|array $plans
14872 */
14873 private function _set_account( FS_User $user, FS_Site $site, $plans = false ) {
14874 $site->user_id = $user->id;
14875
14876 $this->_site = $site;
14877 $this->_user = $user;
14878 if ( false !== $plans ) {
14879 $this->_plans = $plans;
14880 }
14881
14882 $this->send_install_update();
14883
14884 $this->_store_account();
14885
14886 }
14887
14888 /**
14889 * Get a sanitized array with the WordPress version, SDK version, and PHP version.
14890 * Each version is trimmed after the 16th char.
14891 *
14892 * @author Vova Feldman (@svovaf)
14893 * @since 2.2.1
14894 *
14895 * @return array
14896 */
14897 private function get_versions() {
14898 $versions = array();
14899 $versions['platform_version'] = get_bloginfo( 'version' );
14900 $versions['sdk_version'] = $this->version;
14901 $versions['programming_language_version'] = phpversion();
14902
14903 foreach ( $versions as $k => $version ) {
14904 if ( is_string( $versions[ $k ] ) && ! empty( $versions[ $k ] ) ) {
14905 $versions[ $k ] = substr( $versions[ $k ], 0, 16 );
14906 }
14907 }
14908
14909 return $versions;
14910 }
14911
14912 /**
14913 * @author Leo Fajardo (@leorw)
14914 * @since 2.3.0
14915 *
14916 * @return bool
14917 */
14918 function has_beta_update() {
14919 return (
14920 ! empty( $this->_storage->beta_data ) &&
14921 ( true === $this->_storage->beta_data['is_beta'] ) &&
14922 version_compare( $this->_storage->beta_data['version'], $this->get_plugin_version(), '>' )
14923 );
14924 }
14925
14926 /**
14927 * @author Leo Fajardo (@leorw)
14928 * @since 2.3.0
14929 *
14930 * @return bool
14931 */
14932 function is_beta() {
14933 return (
14934 ! empty( $this->_storage->beta_data ) &&
14935 ( true === $this->_storage->beta_data['is_beta'] ) &&
14936 ( $this->get_plugin_version() === $this->_storage->beta_data['version'] )
14937 );
14938 }
14939
14940 /**
14941 * @author Leo Fajardo (@leorw)
14942 * @since 2.3.0
14943 */
14944 private function sync_user_beta_mode() {
14945 $user = $this->get_api_user_scope()->get( '/?plugin_id=' . $this->get_id() . '&fields=is_beta' );
14946
14947 if ( $this->is_api_result_entity( $user ) ) {
14948 $this->_user->is_beta = $user->is_beta;
14949 $this->_store_user();
14950 }
14951 }
14952
14953 /**
14954 * @author Vova Feldman (@svovaf)
14955 * @since 1.1.7.4
14956 *
14957 * @param array $override_with
14958 * @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.
14959 *
14960 * @return array
14961 */
14962 function get_opt_in_params( $override_with = array(), $network_level_or_blog_id = null ) {
14963 $this->_logger->entrance();
14964
14965 $current_user = self::_get_current_wp_user();
14966
14967 $activation_action = $this->get_unique_affix() . '_activate_new';
14968 $return_url = $this->is_anonymous() ?
14969 // If skipped already, then return to the account page.
14970 $this->get_account_url( $activation_action, array(), false ) :
14971 // Return to the module's main page.
14972 $this->get_after_activation_url( 'after_connect_url', array( 'fs_action' => $activation_action ) );
14973
14974 $versions = $this->get_versions();
14975
14976 $params = array_merge( $versions, array(
14977 'user_firstname' => $current_user->user_firstname,
14978 'user_lastname' => $current_user->user_lastname,
14979 'user_nickname' => $current_user->user_nicename,
14980 'user_email' => $current_user->user_email,
14981 'user_ip' => WP_FS__REMOTE_ADDR,
14982 'plugin_slug' => $this->_slug,
14983 'plugin_id' => $this->get_id(),
14984 'plugin_public_key' => $this->get_public_key(),
14985 'plugin_version' => $this->get_plugin_version(),
14986 'return_url' => fs_nonce_url( $return_url, $activation_action ),
14987 'account_url' => fs_nonce_url( $this->_get_admin_page_url(
14988 'account',
14989 array( 'fs_action' => 'sync_user' )
14990 ), 'sync_user' ),
14991 'is_premium' => $this->is_premium(),
14992 'is_active' => true,
14993 'is_uninstalled' => false,
14994 ) );
14995
14996 if ( true === $network_level_or_blog_id ) {
14997 if ( ! isset( $override_with['sites'] ) ) {
14998 $params['sites'] = $this->get_sites_for_network_level_optin();
14999 }
15000 } else {
15001 $site = is_numeric( $network_level_or_blog_id ) ?
15002 array( 'blog_id' => $network_level_or_blog_id ) :
15003 null;
15004
15005 $site = $this->get_site_info( $site );
15006
15007 $params = array_merge( $params, array(
15008 'site_uid' => $site['uid'],
15009 'site_url' => $site['url'],
15010 'site_name' => $site['title'],
15011 'language' => $site['language'],
15012 'charset' => $site['charset'],
15013 ) );
15014 }
15015
15016 if ( $this->is_pending_activation() &&
15017 ! empty( $this->_storage->pending_license_key )
15018 ) {
15019 $params['license_key'] = $this->_storage->pending_license_key;
15020 }
15021
15022 if ( WP_FS__SKIP_EMAIL_ACTIVATION && $this->has_secret_key() ) {
15023 // Even though rand() is known for its security issues,
15024 // the timestamp adds another layer of protection.
15025 // It would be very hard for an attacker to get the secret key form here.
15026 // Plus, this should never run in production since the secret should never
15027 // be included in the production version.
15028 $params['ts'] = WP_FS__SCRIPT_START_TIME;
15029 $params['salt'] = md5( uniqid( rand() ) );
15030 $params['secure'] = md5(
15031 $params['ts'] .
15032 $params['salt'] .
15033 $this->get_secret_key()
15034 );
15035 }
15036
15037 return array_merge( $params, $override_with );
15038 }
15039
15040 /**
15041 * 1. If successful opt-in or pending activation returns the next page that the user should be redirected to.
15042 * 2. If there was an API error, return the API result.
15043 *
15044 * @author Vova Feldman (@svovaf)
15045 * @since 1.1.7.4
15046 *
15047 * @param string|bool $email
15048 * @param string|bool $first
15049 * @param string|bool $last
15050 * @param string|bool $license_key
15051 * @param bool $is_uninstall If "true", this means that the module is currently being uninstalled.
15052 * In this case, the user and site info will be sent to the server but no
15053 * data will be saved to the WP installation's database.
15054 * @param number|bool $trial_plan_id
15055 * @param bool $is_disconnected Whether or not to opt in without tracking.
15056 * @param null|bool $is_marketing_allowed
15057 * @param array $sites If network-level opt-in, an array of containing details of sites.
15058 *
15059 * @return string|object
15060 * @use WP_Error
15061 */
15062 function opt_in(
15063 $email = false,
15064 $first = false,
15065 $last = false,
15066 $license_key = false,
15067 $is_uninstall = false,
15068 $trial_plan_id = false,
15069 $is_disconnected = false,
15070 $is_marketing_allowed = null,
15071 $sites = array()
15072 ) {
15073 $this->_logger->entrance();
15074
15075 if ( false === $email ) {
15076 $current_user = self::_get_current_wp_user();
15077 $email = $current_user->user_email;
15078 }
15079
15080 /**
15081 * @since 1.2.1 If activating with license key, ignore the context-user
15082 * since the user will be automatically loaded from the license.
15083 */
15084 if ( empty( $license_key ) ) {
15085 // Clean up pending license if opt-ing in again.
15086 $this->_storage->remove( 'pending_license_key' );
15087
15088 if ( ! $is_uninstall ) {
15089 $fs_user = Freemius::_get_user_by_email( $email );
15090 if ( is_object( $fs_user ) && ! $this->is_pending_activation() ) {
15091 return $this->install_with_current_user(
15092 false,
15093 $trial_plan_id,
15094 $sites
15095 );
15096 }
15097 }
15098 }
15099
15100 $user_info = array();
15101 if ( ! empty( $email ) ) {
15102 $user_info['user_email'] = $email;
15103 }
15104 if ( ! empty( $first ) ) {
15105 $user_info['user_firstname'] = $first;
15106 }
15107 if ( ! empty( $last ) ) {
15108 $user_info['user_lastname'] = $last;
15109 }
15110
15111 if ( ! empty( $sites ) ) {
15112 $is_network = true;
15113
15114 $user_info['sites'] = $sites;
15115 } else {
15116 $is_network = false;
15117 }
15118
15119 $params = $this->get_opt_in_params( $user_info, $is_network );
15120
15121 $filtered_license_key = false;
15122 if ( is_string( $license_key ) ) {
15123 $filtered_license_key = $this->apply_filters( 'license_key', $license_key );
15124 $params['license_key'] = $filtered_license_key;
15125 } else if ( FS_Plugin_Plan::is_valid_id( $trial_plan_id ) ) {
15126 $params['trial_plan_id'] = $trial_plan_id;
15127 }
15128
15129 if ( $is_uninstall ) {
15130 $params['uninstall_params'] = array(
15131 'reason_id' => $this->_storage->uninstall_reason->id,
15132 'reason_info' => $this->_storage->uninstall_reason->info
15133 );
15134 }
15135
15136 if ( isset( $params['license_key'] ) ) {
15137 $fs_user = Freemius::_get_user_by_email( $email );
15138
15139 if ( is_object( $fs_user ) ) {
15140 /**
15141 * If opting in with a context license and the context WP Admin user already opted in
15142 * before from the current site, add the user context security params to avoid the
15143 * unnecessary email activation when the context license is owned by the same context user.
15144 *
15145 * @author Leo Fajardo (@leorw)
15146 * @since 1.2.3
15147 */
15148 $params = array_merge( $params, FS_Security::instance()->get_context_params(
15149 $fs_user,
15150 false,
15151 'install_with_existing_user'
15152 ) );
15153 }
15154 }
15155
15156 if ( is_bool( $is_marketing_allowed ) ) {
15157 $params['is_marketing_allowed'] = $is_marketing_allowed;
15158 }
15159
15160 $params['is_disconnected'] = $is_disconnected;
15161 $params['format'] = 'json';
15162
15163 $request = array(
15164 'method' => 'POST',
15165 'body' => $params,
15166 'timeout' => WP_FS__DEBUG_SDK ? 60 : 30,
15167 );
15168
15169 $url = $this->add_show_pending( WP_FS__ADDRESS . '/action/service/user/install/' );
15170 $response = self::safe_remote_post( $url, $request );
15171
15172 if ( is_wp_error( $response ) ) {
15173 /**
15174 * @var WP_Error $response
15175 */
15176 $result = new stdClass();
15177
15178 $error_code = $response->get_error_code();
15179 $error_type = str_replace( ' ', '', ucwords( str_replace( '_', ' ', $error_code ) ) );
15180
15181 $result->error = (object) array(
15182 'type' => $error_type,
15183 'message' => $response->get_error_message(),
15184 'code' => $error_code,
15185 'http' => 402
15186 );
15187
15188 return $result;
15189 }
15190
15191 // Module is being uninstalled, don't handle the returned data.
15192 if ( $is_uninstall ) {
15193 return true;
15194 }
15195
15196 /**
15197 * 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.
15198 *
15199 * @author Vova Feldman (@svovaf)
15200 * @since 1.2.3
15201 * @link https://themes.trac.wordpress.org/ticket/46134#comment:5
15202 * @link https://themes.trac.wordpress.org/ticket/46134#comment:9
15203 * @link https://themes.trac.wordpress.org/ticket/46134#comment:12
15204 * @link https://themes.trac.wordpress.org/ticket/46134#comment:14
15205 */
15206 $decoded = is_string( $response['body'] ) ?
15207 json_decode( $response['body'] ) :
15208 null;
15209
15210 if ( empty( $decoded ) ) {
15211 return false;
15212 }
15213
15214 if ( ! $this->is_api_result_object( $decoded ) ) {
15215 if ( ! empty( $params['license_key'] ) ) {
15216 // Pass the fully entered license key to the failure handler.
15217 $params['license_key'] = $license_key;
15218 }
15219
15220 return $is_uninstall ?
15221 $decoded :
15222 $this->apply_filters( 'after_install_failure', $decoded, $params );
15223 } else if ( isset( $decoded->pending_activation ) && $decoded->pending_activation ) {
15224 if ( $is_network ) {
15225 $site_ids = array();
15226 foreach ( $sites as $site ) {
15227 $site_ids[] = $site['blog_id'];
15228 }
15229
15230 /**
15231 * Store the sites so that they can be installed once the user has clicked on the activation link
15232 * in the email.
15233 *
15234 * @author Leo Fajardo (@leorw)
15235 */
15236 $this->_storage->pending_sites_info = array(
15237 'blog_ids' => $site_ids,
15238 'license_key' => $license_key,
15239 'trial_plan_id' => $trial_plan_id
15240 );
15241 }
15242
15243 // Pending activation, add message.
15244 return $this->set_pending_confirmation(
15245 ( isset( $decoded->email ) ?
15246 $decoded->email :
15247 true ),
15248 false,
15249 $filtered_license_key,
15250 ! empty( $params['trial_plan_id'] )
15251 );
15252 } else if ( isset( $decoded->install_secret_key ) ) {
15253 return $this->install_with_new_user(
15254 $decoded->user_id,
15255 $decoded->user_public_key,
15256 $decoded->user_secret_key,
15257 ( isset( $decoded->is_marketing_allowed ) && ! is_null( $decoded->is_marketing_allowed ) ?
15258 $decoded->is_marketing_allowed :
15259 null ),
15260 $decoded->install_id,
15261 $decoded->install_public_key,
15262 $decoded->install_secret_key,
15263 false
15264 );
15265 } else if ( is_array( $decoded->installs ) ) {
15266 return $this->install_many_with_new_user(
15267 $decoded->user_id,
15268 $decoded->user_public_key,
15269 $decoded->user_secret_key,
15270 ( isset( $decoded->is_marketing_allowed ) && ! is_null( $decoded->is_marketing_allowed ) ?
15271 $decoded->is_marketing_allowed :
15272 null ),
15273 $decoded->installs,
15274 false
15275 );
15276 }
15277
15278 return $decoded;
15279 }
15280
15281 /**
15282 * Set user and site identities.
15283 *
15284 * @author Vova Feldman (@svovaf)
15285 * @since 1.0.9
15286 *
15287 * @param FS_User $user
15288 * @param FS_Site $site
15289 * @param bool $redirect
15290 * @param bool $auto_install Since 1.2.1.7 If `true` and setting up an account with a valid license, will
15291 * redirect (or return a URL) to the account page with a special parameter to
15292 * trigger the auto installation processes.
15293 *
15294 * @return string If redirect is `false`, returns the next page the user should be redirected to.
15295 */
15296 function setup_account(
15297 FS_User $user,
15298 FS_Site $site,
15299 $redirect = true,
15300 $auto_install = false
15301 ) {
15302 return $this->setup_network_account(
15303 $user,
15304 array( $site ),
15305 $redirect,
15306 $auto_install,
15307 false
15308 );
15309 }
15310
15311 /**
15312 * Set user and site identities.
15313 *
15314 * @author Vova Feldman (@svovaf)
15315 * @since 2.0.0
15316 *
15317 * @param FS_User $user
15318 * @param FS_Site[] $installs
15319 * @param bool $redirect
15320 * @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.
15321 * @param bool $is_network_level_opt_in
15322 *
15323 * @return string If redirect is `false`, returns the next page the user should be redirected to.
15324 */
15325 function setup_network_account(
15326 FS_User $user,
15327 array $installs,
15328 $redirect = true,
15329 $auto_install = false,
15330 $is_network_level_opt_in = true
15331 ) {
15332 $first_install = $installs[0];
15333
15334 $this->_user = $user;
15335 $this->_site = $first_install;
15336
15337 $this->_sync_plans();
15338
15339 if ( $this->_storage->handle_gdpr_admin_notice &&
15340 $this->should_handle_gdpr_admin_notice() &&
15341 FS_GDPR_Manager::instance()->should_show_opt_in_notice()
15342 ) {
15343 /**
15344 * Clear user lock after an opt-in.
15345 */
15346 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
15347 FS_User_Lock::instance()->unlock();
15348 }
15349
15350 if ( 1 < count( $installs ) ) {
15351 // Only network level opt-in can have more than one install.
15352 $is_network_level_opt_in = true;
15353 }
15354 // $is_network_level_opt_in = self::is_ajax_action_static( 'network_activate', $this->_module_id );
15355 // If Freemius was OFF before, turn it on.
15356 $this->turn_on();
15357
15358 $this->handle_account_connection(
15359 $installs,
15360 ( ! $this->_is_network_active || ! $is_network_level_opt_in )
15361 );
15362
15363 if ( is_numeric( $first_install->license_id ) ) {
15364 $this->_license = $this->_get_license_by_id( $first_install->license_id );
15365 }
15366
15367 $this->_admin_notices->remove_sticky( 'connect_account' );
15368
15369 if ( $this->is_pending_activation() || ! $this->has_settings_menu() ) {
15370 // Remove pending activation sticky notice (if still exist).
15371 $this->_admin_notices->remove_sticky( 'activation_pending' );
15372
15373 // Remove plugin from pending activation mode.
15374 unset( $this->_storage->is_pending_activation );
15375
15376 if ( ! $this->is_paying_or_trial() ) {
15377 $this->_admin_notices->add_sticky(
15378 sprintf( $this->get_text_inline( '%s activation was successfully completed.', 'plugin-x-activation-message' ), '<b>' . $this->get_plugin_name() . '</b>' ),
15379 'activation_complete'
15380 );
15381 }
15382 }
15383
15384 if ( $this->is_paying_or_trial() ) {
15385 if ( ! $this->is_premium() ||
15386 ! $this->has_premium_version() ||
15387 ! $this->has_settings_menu()
15388 ) {
15389 if ( $this->is_paying() ) {
15390 $this->_admin_notices->add_sticky(
15391 sprintf(
15392 $this->get_text_inline( 'Your account was successfully activated with the %s plan.', 'activation-with-plan-x-message' ),
15393 $this->get_plan_title()
15394 ) . $this->get_complete_upgrade_instructions(),
15395 'plan_upgraded',
15396 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
15397 );
15398 } else {
15399 $trial_plan = $this->get_trial_plan();
15400
15401 $this->_admin_notices->add_sticky(
15402 sprintf(
15403 $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ),
15404 '<i>' . $this->get_plugin_name() . '</i>'
15405 ) . $this->get_complete_upgrade_instructions( $trial_plan->title ),
15406 'trial_started',
15407 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
15408 );
15409 }
15410 }
15411
15412 $this->_admin_notices->remove_sticky( array(
15413 'trial_promotion',
15414 ) );
15415 }
15416
15417 $plugin_id = fs_request_get( 'plugin_id', false );
15418
15419 // Store activation time ONLY for plugins & themes (not add-ons).
15420 if ( ! is_numeric( $plugin_id ) || ( $plugin_id == $this->_plugin->id ) ) {
15421 if ( empty( $this->_storage->activation_timestamp ) ) {
15422 $this->_storage->activation_timestamp = WP_FS__SCRIPT_START_TIME;
15423 }
15424 }
15425
15426 $next_page = '';
15427
15428 $extra = array();
15429 if ( $auto_install ) {
15430 $extra['auto_install'] = 'true';
15431 }
15432
15433 if ( is_numeric( $plugin_id ) ) {
15434 /**
15435 * @author Leo Fajardo (@leorw)
15436 * @since 1.2.1.6
15437 *
15438 * Also sync the license after an anonymous user subscribes.
15439 */
15440 if ( $this->is_anonymous() || $plugin_id != $this->_plugin->id ) {
15441 // Add-on was installed - sync license right after install.
15442 $next_page = $this->_get_sync_license_url( $plugin_id, true, $extra );
15443 }
15444 } else {
15445 /**
15446 * @author Vova Feldman (@svovaf)
15447 * @since 1.1.9 If site installed with a valid license, sync license.
15448 */
15449 if ( $this->is_paying() ) {
15450 $this->_sync_plugin_license(
15451 true,
15452 // Installs data is already synced in the beginning of this method directly or via _set_account().
15453 false
15454 );
15455 }
15456
15457 // Reload the page with the keys.
15458 $next_page = $this->is_anonymous() ?
15459 // If user previously skipped, redirect to account page.
15460 $this->get_account_url( false, $extra ) :
15461 $this->get_after_activation_url( 'after_connect_url', array(), $is_network_level_opt_in );
15462 }
15463
15464 if ( ! empty( $next_page ) && $redirect ) {
15465 fs_redirect( $next_page );
15466 }
15467
15468 return $next_page;
15469 }
15470
15471 /**
15472 * Install plugin with new user information after approval.
15473 *
15474 * @author Vova Feldman (@svovaf)
15475 * @since 1.0.7
15476 */
15477 function _install_with_new_user() {
15478 $this->_logger->entrance();
15479
15480 if ( $this->is_registered() ) {
15481 return;
15482 }
15483
15484 if ( ( $this->is_plugin() && fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) ||
15485 // @todo This logic should be improved because it's executed on every load of a theme.
15486 $this->is_theme()
15487 ) {
15488 // check_admin_referer( $this->_slug . '_activate_new' );
15489
15490 if ( fs_request_has( 'user_secret_key' ) ) {
15491 if ( fs_is_network_admin() && isset( $this->_storage->pending_sites_info ) ) {
15492 $pending_sites_info = $this->_storage->pending_sites_info;
15493
15494 $this->install_many_pending_with_user(
15495 fs_request_get( 'user_id' ),
15496 fs_request_get( 'user_public_key' ),
15497 fs_request_get( 'user_secret_key' ),
15498 fs_request_get_bool( 'is_marketing_allowed', null ),
15499 $pending_sites_info['blog_ids'],
15500 $pending_sites_info['license_key'],
15501 $pending_sites_info['trial_plan_id']
15502 );
15503 } else {
15504 $this->install_with_new_user(
15505 fs_request_get( 'user_id' ),
15506 fs_request_get( 'user_public_key' ),
15507 fs_request_get( 'user_secret_key' ),
15508 fs_request_get_bool( 'is_marketing_allowed', null ),
15509 fs_request_get( 'install_id' ),
15510 fs_request_get( 'install_public_key' ),
15511 fs_request_get( 'install_secret_key' ),
15512 true,
15513 fs_request_get_bool( 'auto_install' )
15514 );
15515 }
15516 } else if ( fs_request_has( 'pending_activation' ) ) {
15517 $this->set_pending_confirmation( fs_request_get( 'user_email' ), true );
15518 }
15519 }
15520 }
15521
15522 /**
15523 * @author Vova Feldman (@svovaf)
15524 * @since 2.0.0
15525 *
15526 * @param number $id
15527 * @param string $public_key
15528 * @param string $secret_key
15529 *
15530 * @return \FS_User
15531 */
15532 private function setup_user( $id, $public_key, $secret_key ) {
15533 $user = self::_get_user_by_id( $id );
15534
15535 if ( is_object( $user ) ) {
15536 $this->_user = $user;
15537 } else {
15538 $user = new FS_User();
15539 $user->id = $id;
15540 $user->public_key = $public_key;
15541 $user->secret_key = $secret_key;
15542
15543 $this->_user = $user;
15544 $user_result = $this->get_api_user_scope()->get();
15545 $user = new FS_User( $user_result );
15546
15547 $this->_user = $user;
15548 $this->_store_user();
15549 }
15550
15551 return $user;
15552 }
15553
15554 /**
15555 * Install plugin with new user.
15556 *
15557 * @author Vova Feldman (@svovaf)
15558 * @since 1.1.7.4
15559 *
15560 * @param number $user_id
15561 * @param string $user_public_key
15562 * @param string $user_secret_key
15563 * @param bool|null $is_marketing_allowed
15564 * @param number $install_id
15565 * @param string $install_public_key
15566 * @param string $install_secret_key
15567 * @param bool $redirect
15568 * @param bool $auto_install Since 1.2.1.7 If `true` and setting up an account with a valid license, will
15569 * redirect (or return a URL) to the account page with a special parameter to
15570 * trigger the auto installation processes.
15571 *
15572 * @return string If redirect is `false`, returns the next page the user should be redirected to.
15573 */
15574 private function install_with_new_user(
15575 $user_id,
15576 $user_public_key,
15577 $user_secret_key,
15578 $is_marketing_allowed,
15579 $install_id,
15580 $install_public_key,
15581 $install_secret_key,
15582 $redirect = true,
15583 $auto_install = false
15584 ) {
15585 /**
15586 * This method is also executed after opting in with a license key since the
15587 * license can be potentially associated with a different owner.
15588 *
15589 * @since 2.0.0
15590 */
15591 $user = self::_get_user_by_id( $user_id );
15592
15593 if ( ! is_object( $user ) ) {
15594 $user = new FS_User();
15595 $user->id = $user_id;
15596 $user->public_key = $user_public_key;
15597 $user->secret_key = $user_secret_key;
15598
15599 $this->_user = $user;
15600 $user_result = $this->get_api_user_scope()->get();
15601 $user = new FS_User( $user_result );
15602 }
15603
15604 $this->_user = $user;
15605
15606 $site = new FS_Site();
15607 $site->id = $install_id;
15608 $site->public_key = $install_public_key;
15609 $site->secret_key = $install_secret_key;
15610
15611 $this->_site = $site;
15612 $site_result = $this->get_api_site_scope()->get();
15613 $site = new FS_Site( $site_result );
15614 $this->_site = $site;
15615
15616 if ( ! is_null( $is_marketing_allowed ) ) {
15617 $this->disable_opt_in_notice_and_lock_user();
15618 }
15619
15620 return $this->setup_account(
15621 $this->_user,
15622 $this->_site,
15623 $redirect,
15624 $auto_install
15625 );
15626 }
15627
15628 /**
15629 * Install plugin with user.
15630 *
15631 * @author Leo Fajardo (@leorw)
15632 * @since 2.0.0
15633 *
15634 * @param number $user_id
15635 * @param string $user_public_key
15636 * @param string $user_secret_key
15637 * @param bool|null $is_marketing_allowed
15638 * @param array $site_ids
15639 * @param bool $license_key
15640 * @param bool $trial_plan_id
15641 * @param bool $redirect
15642 *
15643 * @return string If redirect is `false`, returns the next page the user should be redirected to.
15644 */
15645 private function install_many_pending_with_user(
15646 $user_id,
15647 $user_public_key,
15648 $user_secret_key,
15649 $is_marketing_allowed,
15650 $site_ids,
15651 $license_key = false,
15652 $trial_plan_id = false,
15653 $redirect = true
15654 ) {
15655 $user = $this->setup_user( $user_id, $user_public_key, $user_secret_key );
15656
15657 if ( ! is_null( $is_marketing_allowed ) ) {
15658 $this->disable_opt_in_notice_and_lock_user();
15659 }
15660
15661 $sites = array();
15662 foreach ( $site_ids as $site_id ) {
15663 $sites[] = $this->get_site_info( array( 'blog_id' => $site_id ) );
15664 }
15665
15666 $this->install_with_user( $user, $license_key, $trial_plan_id, $redirect, true, $sites );
15667 }
15668
15669 /**
15670 * Multi-site install with a new user.
15671 *
15672 * @author Vova Feldman (@svovaf)
15673 * @since 2.0.0
15674 *
15675 * @param number $user_id
15676 * @param string $user_public_key
15677 * @param string $user_secret_key
15678 * @param bool|null $is_marketing_allowed
15679 * @param object[] $installs
15680 * @param bool $redirect
15681 * @param bool $auto_install Since 1.2.1.7 If `true` and setting up an account with a valid license, will
15682 * redirect (or return a URL) to the account page with a special parameter to
15683 * trigger the auto installation processes.
15684 *
15685 * @return string If redirect is `false`, returns the next page the user should be redirected to.
15686 */
15687 private function install_many_with_new_user(
15688 $user_id,
15689 $user_public_key,
15690 $user_secret_key,
15691 $is_marketing_allowed,
15692 array $installs,
15693 $redirect = true,
15694 $auto_install = false
15695 ) {
15696 $this->setup_user( $user_id, $user_public_key, $user_secret_key );
15697
15698 if ( ! is_null( $is_marketing_allowed ) ) {
15699 $this->disable_opt_in_notice_and_lock_user();
15700 }
15701
15702 $install_ids = array();
15703
15704 foreach ( $installs as $install ) {
15705 $install_ids[] = $install->id;
15706 }
15707
15708 $left = count( $install_ids );
15709 $offset = 0;
15710
15711 $installs = array();
15712 while ( $left > 0 ) {
15713 $result = $this->get_api_user_scope()->get( "/plugins/{$this->_module_id}/installs.json?ids=" . implode( ',', array_slice( $install_ids, $offset, 25 ) ) );
15714
15715 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
15716 // @todo Handle API error.
15717 }
15718
15719 $installs = array_merge( $installs, $result->installs );
15720
15721 $left -= 25;
15722 }
15723
15724 foreach ( $installs as &$install ) {
15725 $install = new FS_Site( $install );
15726 }
15727
15728 return $this->setup_network_account(
15729 $this->_user,
15730 $installs,
15731 $redirect,
15732 $auto_install
15733 );
15734 }
15735
15736 /**
15737 * @author Vova Feldman (@svovaf)
15738 * @since 1.1.7.4
15739 *
15740 * @param string|bool $email
15741 * @param bool $redirect
15742 * @param string|bool $license_key Since 1.2.1.5
15743 * @param bool $is_pending_trial Since 1.2.1.5
15744 *
15745 * @return string Since 1.2.1.5 if $redirect is `false`, return the pending activation page.
15746 */
15747 private function set_pending_confirmation(
15748 $email = false,
15749 $redirect = true,
15750 $license_key = false,
15751 $is_pending_trial = false
15752 ) {
15753 if ( $this->_ignore_pending_mode ) {
15754 /**
15755 * If explicitly asked to ignore pending mode, set to anonymous mode
15756 * if require confirmation before finalizing the opt-in.
15757 *
15758 * @author Vova Feldman
15759 * @since 1.2.1.6
15760 */
15761 $this->skip_connection( null, fs_is_network_admin() );
15762 } else {
15763 // Install must be activated via email since
15764 // user with the same email already exist.
15765 $this->_storage->is_pending_activation = true;
15766 $this->_add_pending_activation_notice( $email, $is_pending_trial );
15767 }
15768
15769 if ( ! empty( $license_key ) ) {
15770 $this->_storage->pending_license_key = $license_key;
15771 }
15772
15773 // Remove the opt-in sticky notice.
15774 $this->_admin_notices->remove_sticky( array(
15775 'connect_account',
15776 'trial_promotion',
15777 ) );
15778
15779 $next_page = $this->get_after_activation_url( 'after_pending_connect_url' );
15780
15781 // Reload the page with with pending activation message.
15782 if ( $redirect ) {
15783 fs_redirect( $next_page );
15784 }
15785
15786 return $next_page;
15787 }
15788
15789 /**
15790 * Install plugin with current logged WP user info.
15791 *
15792 * @author Vova Feldman (@svovaf)
15793 * @since 1.0.7
15794 */
15795 function _install_with_current_user() {
15796 $this->_logger->entrance();
15797
15798 if ( $this->is_registered() ) {
15799 return;
15800 }
15801
15802 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) && fs_request_is_post() ) {
15803 // check_admin_referer( 'activate_existing_' . $this->_plugin->public_key );
15804
15805 /**
15806 * @author Vova Feldman (@svovaf)
15807 * @since 1.1.9 Add license key if given.
15808 */
15809 $license_key = fs_request_get( 'license_secret_key' );
15810
15811 $this->install_with_current_user( $license_key );
15812 }
15813 }
15814
15815
15816 /**
15817 * @author Vova Feldman (@svovaf)
15818 * @since 1.1.7.4
15819 *
15820 * @param string|bool $license_key
15821 * @param number|bool $trial_plan_id
15822 * @param array $sites Since 2.0.0
15823 * @param bool $redirect
15824 *
15825 * @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.
15826 */
15827 private function install_with_current_user(
15828 $license_key = false,
15829 $trial_plan_id = false,
15830 $sites = array(),
15831 $redirect = true
15832 ) {
15833 // Get current logged WP user.
15834 $current_user = self::_get_current_wp_user();
15835
15836 // Find the relevant FS user by the email.
15837 $user = self::_get_user_by_email( $current_user->user_email );
15838
15839 return $this->install_with_user( $user, $license_key, $trial_plan_id, $redirect, true, $sites );
15840 }
15841
15842 /**
15843 * @author Vova Feldman (@svovaf)
15844 * @since 2.0.0
15845 *
15846 * @param \FS_User $user
15847 * @param string|bool $license_key
15848 * @param number|bool $trial_plan_id
15849 * @param bool $redirect
15850 * @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.
15851 * @param array $sites Since 2.0.0. If not empty, should be a collection of site details for the bulk install API request.
15852 *
15853 * @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.
15854 */
15855 private function install_with_user(
15856 FS_User $user,
15857 $license_key = false,
15858 $trial_plan_id = false,
15859 $redirect = true,
15860 $setup_account = true,
15861 $sites = array()
15862 ) {
15863 // We have to set the user before getting user scope API handler.
15864 $this->_user = $user;
15865
15866 // Install the plugin.
15867 $result = $this->create_installs_with_user(
15868 $user,
15869 $license_key,
15870 $trial_plan_id,
15871 $sites,
15872 $redirect
15873 );
15874
15875 if ( ! $this->is_api_result_entity( $result ) &&
15876 ! $this->is_api_result_object( $result, 'installs' )
15877 ) {
15878 // @todo Handler potential API error of the $result
15879 }
15880
15881 if ( empty( $sites ) ) {
15882 $site = new FS_Site( $result );
15883 $this->_site = $site;
15884
15885 if ( ! $setup_account ) {
15886 $this->_store_site();
15887
15888 $this->sync_plan_if_not_exist( $site->plan_id );
15889
15890 if ( ! empty( $license_key ) && FS_Plugin_License::is_valid_id( $site->license_id ) ) {
15891 $this->sync_license_if_not_exist( $site->license_id, $license_key );
15892 }
15893
15894 $this->_admin_notices->remove_sticky( 'connect_account', false );
15895
15896 return $site;
15897 }
15898
15899 return $this->setup_account( $this->_user, $this->_site, $redirect );
15900 } else {
15901 $installs = array();
15902 foreach ( $result->installs as $install ) {
15903 $installs[] = new FS_Site( $install );
15904 }
15905
15906 return $this->setup_network_account(
15907 $user,
15908 $installs,
15909 $redirect
15910 );
15911 }
15912 }
15913
15914 /**
15915 * Initiate an API request to create a collection of installs.
15916 *
15917 * @author Vova Feldman (@svovaf)
15918 * @since 2.0.0
15919 *
15920 * @param \FS_User $user
15921 * @param bool $license_key
15922 * @param bool $trial_plan_id
15923 * @param array $sites
15924 * @param bool $redirect
15925 * @param bool $silent
15926 *
15927 * @return object|mixed
15928 */
15929 private function create_installs_with_user(
15930 FS_User $user,
15931 $license_key = false,
15932 $trial_plan_id = false,
15933 $sites = array(),
15934 $redirect = false,
15935 $silent = false
15936 ) {
15937 $extra_install_params = array(
15938 'uid' => $this->get_anonymous_id(),
15939 'is_disconnected' => false,
15940 );
15941
15942 if ( ! empty( $license_key ) ) {
15943 $extra_install_params['license_key'] = $this->apply_filters( 'license_key', $license_key );
15944 } else if ( FS_Plugin_Plan::is_valid_id( $trial_plan_id ) ) {
15945 $extra_install_params['trial_plan_id'] = $trial_plan_id;
15946 }
15947
15948 if ( ! empty( $sites ) ) {
15949 $extra_install_params['sites'] = $sites;
15950 }
15951
15952 $args = $this->get_install_data_for_api( $extra_install_params, false, false );
15953
15954 // Install the plugin.
15955 $result = $this->get_api_user_scope_by_user( $user )->call(
15956 "/plugins/{$this->get_id()}/installs.json",
15957 'post',
15958 $args
15959 );
15960
15961 if ( ! $this->is_api_result_entity( $result ) &&
15962 ! $this->is_api_result_object( $result, 'installs' )
15963 ) {
15964 if ( ! empty( $args['license_key'] ) ) {
15965 // Pass full the fully entered license key to the failure handler.
15966 $args['license_key'] = $license_key;
15967 }
15968
15969 $result = $this->apply_filters( 'after_install_failure', $result, $args );
15970
15971 if ( ! $silent ) {
15972 $this->_admin_notices->add(
15973 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
15974 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $result->error->message . '</b>',
15975 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
15976 'error'
15977 );
15978 }
15979
15980 if ( $redirect ) {
15981 /**
15982 * We set the user before getting the user scope API handler, so the user became temporarily
15983 * registered (`is_registered() = true`). Since the API returned an error and we will redirect,
15984 * we have to set the user to `null`, otherwise, the user will be redirected to the wrong
15985 * activation page based on the return value of `is_registered()`. In addition, in case the
15986 * context plugin doesn't have a settings menu and the default page is the `Plugins` page,
15987 * misleading plugin activation errors will be shown on the `Plugins` page.
15988 *
15989 * @author Leo Fajardo (@leorw)
15990 */
15991 $this->_user = null;
15992
15993 fs_redirect( $this->get_activation_url( array( 'error' => $result->error->message ) ) );
15994 }
15995 }
15996
15997 return $result;
15998 }
15999
16000 /**
16001 * Tries to activate add-on account based on parent plugin info.
16002 *
16003 * @author Vova Feldman (@svovaf)
16004 * @since 1.0.6
16005 *
16006 * @param Freemius $parent_fs
16007 * @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.
16008 */
16009 private function _activate_addon_account( Freemius $parent_fs, $network_level_or_blog_id = null ) {
16010 if ( $this->is_registered() ) {
16011 // Already activated.
16012 return;
16013 }
16014
16015 /**
16016 * Do not override the `uid` if network-level opt-in since the call to `get_sites_for_network_level_optin()`
16017 * already returns the data for the current blog.
16018 *
16019 * @author Leo Fajardo (@leorw)
16020 * @since 2.3.0
16021 */
16022 $uid_param_to_override = ( true === $network_level_or_blog_id ) ?
16023 array() :
16024 array( 'uid' => $this->get_anonymous_id() );
16025
16026 $params = $this->get_install_data_for_api(
16027 $uid_param_to_override,
16028 false,
16029 false,
16030 /**
16031 * Do not include the data for the current blog if network-level opt-in since the call to `get_sites_for_network_level_optin`
16032 * already includes the data for it.
16033 *
16034 * @author Leo Fajardo (@leorw)
16035 * @since 2.3.0
16036 */
16037 ( true !== $network_level_or_blog_id )
16038 );
16039
16040 if ( true === $network_level_or_blog_id ) {
16041 $params['sites'] = $this->get_sites_for_network_level_optin();
16042
16043 if ( empty( $params['sites'] ) ) {
16044 return;
16045 }
16046 }
16047
16048 // Activate add-on with parent plugin credentials.
16049 $result = $parent_fs->get_api_site_scope()->call(
16050 "/addons/{$this->_plugin->id}/installs.json",
16051 'post',
16052 $params
16053 );
16054
16055 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
16056 $error_message = FS_Api::is_api_error_object( $result ) ?
16057 $result->error->message :
16058 $this->get_text_inline( 'An unknown error has occurred.', 'unknown-error' );
16059
16060 $this->_admin_notices->add(
16061 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
16062 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $error_message . '</b>',
16063 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
16064 'error'
16065 );
16066
16067 return;
16068 }
16069
16070 $addon_installs = $result->installs;
16071 foreach ( $addon_installs as $key => $addon_install ) {
16072 $addon_installs[ $key ] = new FS_Site( $addon_install );
16073 }
16074
16075 $first_install = $addon_installs[0];
16076
16077 // Get user information based on parent's plugin.
16078 $user = $parent_fs->get_user();
16079
16080 // First of all, set site and user info - otherwise we won't
16081 // be able to invoke API calls.
16082 $this->_site = $first_install;
16083 $this->_user = $user;
16084
16085 // Sync add-on plans.
16086 $this->_sync_plans();
16087
16088 $this->handle_account_connection( $addon_installs, ! fs_is_network_admin() );
16089
16090 // Get site's current plan.
16091 //$this->_site->plan = $this->_get_plan_by_id( $this->_site->plan->id );
16092
16093 // Sync licenses.
16094 $this->_sync_licenses();
16095
16096 if ( ! fs_is_network_admin() ) {
16097 // Try to activate premium license.
16098 $this->_activate_license( true );
16099 } else {
16100 $license_id = fs_request_get( 'license_id' );
16101
16102 if ( is_object( $this->_site ) &&
16103 FS_Plugin_License::is_valid_id( $license_id ) &&
16104 $license_id == $this->_site->license_id
16105 ) {
16106 // License is already activated.
16107 return;
16108 }
16109
16110 $premium_license = FS_Plugin_License::is_valid_id( $license_id ) ?
16111 $this->_get_license_by_id( $license_id ) :
16112 $this->_get_available_premium_license();
16113
16114 if ( is_object( $premium_license ) ) {
16115 $this->maybe_network_activate_addon_license( $premium_license );
16116 }
16117 }
16118 }
16119
16120 /**
16121 * @author Leo Fajardo (@leorw)
16122 * @since 2.3.0
16123 *
16124 * @param FS_Site[] $installs
16125 * @param bool $is_site_level
16126 */
16127 private function handle_account_connection( $installs, $is_site_level ) {
16128 $first_install = $installs[0];
16129
16130 if ( $is_site_level ) {
16131 $this->_set_account( $this->_user, $first_install );
16132
16133 $this->do_action( 'after_account_connection', $this->_user, $first_install );
16134 } else {
16135 $this->_store_user();
16136
16137 // Map site addresses to their blog IDs.
16138 $address_to_blog_map = $this->get_address_to_blog_map();
16139
16140 $first_blog_id = null;
16141 $blog_2_install_map = array();
16142 foreach ( $installs as $install ) {
16143 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
16144 $blog_id = $address_to_blog_map[ $address ];
16145
16146 $this->_store_site( true, $blog_id, $install );
16147
16148 if ( is_null( $first_blog_id ) ) {
16149 $first_blog_id = $blog_id;
16150 }
16151
16152 $blog_2_install_map[ $blog_id ] = $install;
16153 }
16154
16155 if ( ! FS_User::is_valid_id( $this->_storage->network_user_id ) ||
16156 ! is_object( self::_get_user_by_id( $this->_storage->network_user_id ) )
16157 ) {
16158 // Store network user.
16159 $this->_storage->network_user_id = $this->_user->id;
16160 }
16161
16162 if ( ! FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ) {
16163 $this->_storage->network_install_blog_id = $first_blog_id;
16164 }
16165
16166 if ( count( $installs ) === count( $address_to_blog_map ) ) {
16167 // Super admin opted in for all sites in the network.
16168 $this->_storage->is_network_connected = true;
16169 }
16170
16171 $this->_store_licenses( false );
16172
16173 self::$_accounts->store();
16174
16175 // Don't sync the installs data on network upgrade
16176 if ( ! $this->network_upgrade_mode_completed() ) {
16177 $this->send_installs_update();
16178 }
16179
16180 // Switch install context back to the first install.
16181 $this->_site = $first_install;
16182
16183 $current_blog = get_current_blog_id();
16184
16185 foreach ( $blog_2_install_map as $blog_id => $install ) {
16186 $this->switch_to_blog( $blog_id );
16187
16188 $this->do_action( 'after_account_connection', $this->_user, $install );
16189 }
16190
16191 $this->switch_to_blog( $current_blog );
16192
16193 $this->do_action( 'after_network_account_connection', $this->_user, $blog_2_install_map );
16194 }
16195 }
16196
16197 /**
16198 * Tries to activate parent account based on add-on's info.
16199 *
16200 * @author Vova Feldman (@svovaf)
16201 * @since 1.2.2.7
16202 *
16203 * @param Freemius $parent_fs
16204 */
16205 private function activate_parent_account( Freemius $parent_fs ) {
16206 if ( ! $this->is_addon() ) {
16207 // This is not an add-on.
16208 return;
16209 }
16210
16211 if ( $parent_fs->is_registered() ) {
16212 // Already activated.
16213 return;
16214 }
16215
16216 // Activate parent with add-on's user credentials.
16217 $parent_install = $this->get_api_user_scope()->call(
16218 "/plugins/{$parent_fs->_plugin->id}/installs.json",
16219 'post',
16220 $parent_fs->get_install_data_for_api( array(
16221 'uid' => $parent_fs->get_anonymous_id(),
16222 ), false, false )
16223 );
16224
16225 if ( isset( $parent_install->error ) ) {
16226 $this->_admin_notices->add(
16227 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
16228 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $parent_install->error->message . '</b>',
16229 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
16230 'error'
16231 );
16232
16233 return;
16234 }
16235
16236 $parent_fs->_admin_notices->remove_sticky( 'connect_account' );
16237
16238 if ( $parent_fs->is_pending_activation() ) {
16239 $parent_fs->_admin_notices->remove_sticky( 'activation_pending' );
16240
16241 unset( $parent_fs->_storage->is_pending_activation );
16242 }
16243
16244 // Get user information based on parent's plugin.
16245 $user = $this->get_user();
16246
16247 // First of all, set site info - otherwise we won't
16248 // be able to invoke API calls.
16249 $parent_fs->_site = new FS_Site( $parent_install );
16250 $parent_fs->_user = $user;
16251
16252 // Sync add-on plans.
16253 $parent_fs->_sync_plans();
16254
16255 $parent_fs->_set_account( $user, $parent_fs->_site );
16256 }
16257
16258 #endregion
16259
16260 #----------------------------------------------------------------------------------
16261 #region Admin Menu Items
16262 #----------------------------------------------------------------------------------
16263
16264 private $_menu_items = array();
16265
16266 /**
16267 * @author Vova Feldman (@svovaf)
16268 * @since 1.2.1.8
16269 *
16270 * @return array
16271 */
16272 function get_menu_items() {
16273 return $this->_menu_items;
16274 }
16275
16276 /**
16277 * @author Vova Feldman (@svovaf)
16278 * @since 1.0.7
16279 *
16280 * @return string
16281 */
16282 function get_menu_slug() {
16283 return $this->_menu->get_slug();
16284 }
16285
16286 /**
16287 * @author Vova Feldman (@svovaf)
16288 * @since 1.0.9
16289 */
16290 function _prepare_admin_menu() {
16291 // if ( ! $this->is_on() ) {
16292 // return;
16293 // }
16294
16295 /**
16296 * When running from a site admin with a network activated module and the connection
16297 * was NOT delegated and the user still haven't skipped or opted-in, then hide the
16298 * site level settings.
16299 *
16300 * @author Vova Feldman (@svovaf)
16301 * @since 2.0.0
16302 */
16303 $should_hide_site_admin_settings = (
16304 $this->_is_network_active &&
16305 ! fs_is_network_admin() &&
16306 ! $this->is_delegated_connection() &&
16307 ! $this->is_anonymous() &&
16308 ! $this->is_registered()
16309 );
16310
16311 $should_hide_site_admin_settings = $this->apply_filters( 'should_hide_site_admin_settings_on_network_activation_mode', $should_hide_site_admin_settings );
16312
16313 if ( ( ! $this->has_api_connectivity() && ! $this->is_enable_anonymous() ) ||
16314 $should_hide_site_admin_settings
16315 ) {
16316 $this->_menu->remove_menu_item( $should_hide_site_admin_settings );
16317 } else {
16318 $this->do_action( fs_is_network_admin() ?
16319 'before_network_admin_menu_init' :
16320 'before_admin_menu_init'
16321 );
16322
16323 $this->add_menu_action();
16324
16325 $this->add_network_menu_when_missing();
16326
16327 $this->add_submenu_items();
16328 }
16329 }
16330
16331 /**
16332 * Admin dashboard menu items modifications.
16333 *
16334 * NOTE: admin_menu action executed before admin_init.
16335 *
16336 * @author Vova Feldman (@svovaf)
16337 * @since 1.0.7
16338 *
16339 */
16340 private function add_menu_action() {
16341 if ( $this->is_activation_mode() ) {
16342 if ( $this->is_plugin() || ( $this->has_settings_menu() && ! $this->is_free_wp_org_theme() ) ) {
16343 $this->override_plugin_menu_with_activation();
16344 } else {
16345 /**
16346 * Handle theme opt-in when the opt-in form shows as a dialog box in the themes page.
16347 */
16348 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) ) {
16349 add_action( 'load-themes.php', array( &$this, '_install_with_current_user' ) );
16350 } else if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ||
16351 fs_request_get_bool( 'pending_activation' )
16352 ) {
16353 add_action( 'load-themes.php', array( &$this, '_install_with_new_user' ) );
16354 }
16355 }
16356 } else {
16357 if ( ! $this->is_registered() ) {
16358 // If not registered try to install user.
16359 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) {
16360 $this->_install_with_new_user();
16361 }
16362 } else if (
16363 fs_request_is_action( 'sync_user' ) &&
16364 ( ! $this->has_settings_menu() || $this->is_free_wp_org_theme() )
16365 ) {
16366 $this->_handle_account_user_sync();
16367 }
16368 }
16369 }
16370
16371 /**
16372 * @author Vova Feldman (@svovaf)
16373 * @since 1.0.1
16374 */
16375 function _redirect_on_clicked_menu_link() {
16376 $this->_logger->entrance();
16377
16378 $page = fs_request_get('page');
16379 $page = is_string($page) ? strtolower($page) : '';
16380
16381 $this->_logger->log( 'page = ' . $page );
16382
16383 foreach ( $this->_menu_items as $priority => $items ) {
16384 foreach ( $items as $item ) {
16385 if ( isset( $item['url'] ) ) {
16386 if ( $page === $this->_menu->get_slug( strtolower( $item['menu_slug'] ) ) ) {
16387 $this->_logger->log( 'Redirecting to ' . $item['url'] );
16388
16389 fs_redirect( $item['url'] );
16390 }
16391 }
16392 }
16393 }
16394 }
16395
16396 /**
16397 * Remove plugin's all admin menu items & pages, and replace with activation page.
16398 *
16399 * @author Vova Feldman (@svovaf)
16400 * @since 1.0.1
16401 */
16402 private function override_plugin_menu_with_activation() {
16403 $this->_logger->entrance();
16404
16405 $hook = false;
16406
16407 if ( ! $this->has_settings_menu() ) {
16408 // Add the opt-in page without a menu item.
16409 $hook = FS_Admin_Menu_Manager::add_subpage(
16410 null,
16411 $this->get_plugin_name(),
16412 $this->get_plugin_name(),
16413 'manage_options',
16414 $this->_slug,
16415 array( &$this, '_connect_page_render' )
16416 );
16417 } else if ( $this->_menu->is_top_level() ) {
16418 if ( $this->_menu->is_override_exact() ) {
16419 // Make sure the current page is matching the activation page.
16420 if ( ! $this->is_matching_url( $this->get_activation_url() ) ) {
16421 return;
16422 }
16423 }
16424
16425 $hook = $this->_menu->override_menu_item( array( &$this, '_connect_page_render' ) );
16426
16427 if ( false === $hook ) {
16428 // Create new menu item just for the opt-in.
16429 $hook = FS_Admin_Menu_Manager::add_page(
16430 $this->get_plugin_name(),
16431 $this->get_plugin_name(),
16432 'manage_options',
16433 $this->_menu->get_slug(),
16434 array( &$this, '_connect_page_render' )
16435 );
16436 }
16437 } else {
16438 $menus = array( $this->_menu->get_parent_slug() );
16439
16440 if ( $this->_menu->is_override_exact() ) {
16441 // Make sure the current page is matching the activation page.
16442 if ( ! $this->is_matching_url( $this->get_activation_url() ) ) {
16443 return;
16444 }
16445 }
16446
16447 foreach ( $menus as $parent_slug ) {
16448 $hook = $this->_menu->override_submenu_action(
16449 $parent_slug,
16450 $this->_menu->get_raw_slug(),
16451 array( &$this, '_connect_page_render' )
16452 );
16453
16454 if ( false !== $hook ) {
16455 // Found plugin's submenu item.
16456 break;
16457 }
16458 }
16459 }
16460
16461 if ( $this->is_activation_page() ) {
16462 // Clean admin page from distracting content.
16463 self::_clean_admin_content_section();
16464 }
16465
16466 if ( false !== $hook ) {
16467 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) ) {
16468 $this->_install_with_current_user();
16469 } else if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) {
16470 $this->_install_with_new_user();
16471 }
16472 }
16473 }
16474
16475 /**
16476 * If a plugin was network activated and connected but don't have a network
16477 * level settings, then add an artificial menu item for the Account and other
16478 * Freemius settings.
16479 *
16480 * @author Vova Feldman (@svovaf)
16481 * @since 2.0.0
16482 */
16483 private function add_network_menu_when_missing() {
16484 $this->_logger->entrance();
16485
16486 if ( ! $this->_is_network_active ) {
16487 // Plugin wasn't activated on the network level.
16488 return;
16489 }
16490
16491 if ( ! fs_is_network_admin() ) {
16492 // The context is not the network admin.
16493 return;
16494 }
16495
16496 if ( $this->_menu->has_network_menu() ) {
16497 // Plugin already has a network level menu.
16498 return;
16499 }
16500
16501 if ( $this->is_network_activation_mode() ) {
16502 /**
16503 * Do not add during activation mode, otherwise, there will be duplicate menus while the opt-in
16504 * screen is being shown.
16505 *
16506 * @author Leo Fajardo (@leorw)
16507 */
16508 return;
16509 }
16510
16511 if ( ! WP_FS__SHOW_NETWORK_EVEN_WHEN_DELEGATED ) {
16512 if ( $this->is_network_delegated_connection() ) {
16513 // Super-admin delegated the connection to the site admins.
16514 return;
16515 }
16516 }
16517
16518 if ( ! $this->_menu->has_menu() || $this->_menu->is_top_level() ) {
16519
16520 if ( $this->_menu->has_menu() ||
16521 ! $this->is_addon() ||
16522 $this->is_activation_mode()
16523 ) {
16524 $this->_dynamically_added_top_level_page_hook_name = $this->_menu->add_page_and_update(
16525 $this->get_plugin_name(),
16526 $this->get_plugin_name(),
16527 'manage_options',
16528 $this->_menu->has_menu() ? $this->_menu->get_slug() : $this->_slug
16529 );
16530 }
16531 } else {
16532 $this->_menu->add_subpage_and_update(
16533 $this->_menu->get_parent_slug(),
16534 $this->get_plugin_name(),
16535 $this->get_plugin_name(),
16536 'manage_options',
16537 $this->_menu->get_slug()
16538 );
16539 }
16540 }
16541
16542 /**
16543 * @author Leo Fajardo (@leorw)
16544 * @since 1.2.1
16545 *
16546 * return string
16547 */
16548 function get_top_level_menu_capability() {
16549 global $menu;
16550
16551 $top_level_menu_slug = $this->get_top_level_menu_slug();
16552
16553 foreach ( $menu as $menu_info ) {
16554 /**
16555 * The second element in the menu info array is the capability/role that has access to the menu and the
16556 * third element is the menu slug.
16557 */
16558 if ( $menu_info[2] === $top_level_menu_slug ) {
16559 return $menu_info[1];
16560 }
16561 }
16562
16563 return 'read';
16564 }
16565
16566 /**
16567 * @author Vova Feldman (@svovaf)
16568 * @since 1.0.0
16569 *
16570 * @return string
16571 */
16572 private function get_top_level_menu_slug() {
16573 return ( $this->is_addon() ?
16574 $this->get_parent_instance()->_menu->get_top_level_menu_slug() :
16575 $this->_menu->get_top_level_menu_slug() );
16576 }
16577
16578 /**
16579 * @author Vova Feldman (@svovaf)
16580 * @since 1.2.2.7
16581 *
16582 * @return string
16583 */
16584 function get_pricing_cta_label() {
16585 $label = $this->get_text_inline( 'Upgrade', 'upgrade' );
16586
16587 if ( $this->is_in_trial_promotion() &&
16588 ! $this->is_paying_or_trial()
16589 ) {
16590 // If running a trial promotion, modify the pricing to load the trial.
16591 $label = $this->get_text_inline( 'Start Trial', 'start-trial' );
16592 } else if ( $this->is_paying() ) {
16593 $label = $this->get_text_inline( 'Pricing', 'pricing' );
16594 }
16595
16596 return $label;
16597 }
16598
16599 /**
16600 * @author Vova Feldman (@svovaf)
16601 * @since 1.2.2.7
16602 *
16603 * @return bool
16604 */
16605 function is_pricing_page_visible() {
16606 return (
16607 // Has at least one paid plan.
16608 $this->has_paid_plan() &&
16609 // Didn't ask to hide the pricing page.
16610 $this->is_page_visible( 'pricing' ) &&
16611 // Don't have a valid active license or has more than one plan.
16612 ( ! $this->is_paying() || ! $this->is_single_plan() )
16613 );
16614 }
16615
16616 /**
16617 * @author Leo Fajardo (@leorw)
16618 * @since 2.3.0
16619 *
16620 * @param bool $is_activation_mode
16621 *
16622 * @return bool
16623 */
16624 private function should_add_submenu_or_action_links( $is_activation_mode ) {
16625 if ( $this->is_addon() ) {
16626 // No submenu items or action links for add-ons.
16627 return false;
16628 }
16629
16630 if ( $this->is_free_wp_org_theme() && ! fs_is_network_admin() ) {
16631 // Also add action links or submenu items when running in a free .org theme so the tabs will be visible.
16632 return true;
16633 }
16634
16635 if ( $is_activation_mode && ! $this->is_free_wp_org_theme() ) {
16636 return false;
16637 }
16638
16639 if ( fs_is_network_admin() ) {
16640 /**
16641 * Add submenu items or action links to network level when plugin was network activated and the super
16642 * admin did NOT delegate the connection of all sites to site admins.
16643 */
16644 return (
16645 $this->_is_network_active &&
16646 ( WP_FS__SHOW_NETWORK_EVEN_WHEN_DELEGATED ||
16647 ! $this->is_network_delegated_connection() )
16648 );
16649 }
16650
16651 return ( ! $this->_is_network_active || $this->is_delegated_connection() );
16652 }
16653
16654 /**
16655 * Add default Freemius menu items.
16656 *
16657 * @author Vova Feldman (@svovaf)
16658 * @since 1.0.0
16659 * @since 1.2.2.7 Also add submenu items when running in a free .org theme so the tabs will be visible.
16660 */
16661 private function add_submenu_items() {
16662 $this->_logger->entrance();
16663
16664 $is_activation_mode = $this->is_activation_mode();
16665
16666 $add_submenu_items = $this->should_add_submenu_or_action_links( $is_activation_mode );
16667
16668 if ( $add_submenu_items ) {
16669 if ( $this->has_affiliate_program() ) {
16670 // Add affiliation page.
16671 $this->add_submenu_item(
16672 $this->get_text_inline( 'Affiliation', 'affiliation' ),
16673 array( &$this, '_affiliation_page_render' ),
16674 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Affiliation', 'affiliation' ),
16675 'manage_options',
16676 'affiliation',
16677 'Freemius::_clean_admin_content_section',
16678 WP_FS__DEFAULT_PRIORITY,
16679 $this->is_submenu_item_visible( 'affiliation' )
16680 );
16681 }
16682 }
16683
16684 if ( $add_submenu_items ||
16685 ( $is_activation_mode &&
16686 $this->is_only_premium() &&
16687 $this->is_admin_page( 'account' ) &&
16688 fs_request_is_action( $this->get_unique_affix() . '_sync_license' )
16689 )
16690 ) {
16691 if ( ! WP_FS__DEMO_MODE && $this->is_registered() ) {
16692 $show_account = (
16693 $this->is_submenu_item_visible( 'account' ) &&
16694 /**
16695 * @since 1.2.2.7 Don't show the Account for free WP.org themes without any paid plans.
16696 */
16697 ( ! $this->is_free_wp_org_theme() || $this->has_paid_plan() )
16698 );
16699
16700 // Add user account page.
16701 $this->add_submenu_item(
16702 $this->get_text_inline( 'Account', 'account' ),
16703 array( &$this, '_account_page_render' ),
16704 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Account', 'account' ),
16705 'manage_options',
16706 'account',
16707 array( &$this, '_account_page_load' ),
16708 WP_FS__DEFAULT_PRIORITY,
16709 ( $add_submenu_items && $show_account )
16710 );
16711 }
16712 }
16713
16714 if ( $add_submenu_items ) {
16715 // Add contact page.
16716 $this->add_submenu_item(
16717 $this->get_text_inline( 'Contact Us', 'contact-us' ),
16718 array( &$this, '_contact_page_render' ),
16719 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Contact Us', 'contact-us' ),
16720 'manage_options',
16721 'contact',
16722 'Freemius::_clean_admin_content_section',
16723 WP_FS__DEFAULT_PRIORITY,
16724 $this->is_submenu_item_visible( 'contact' )
16725 );
16726
16727 if ( $this->has_addons() ) {
16728 $this->add_submenu_item(
16729 $this->get_text_inline( 'Add-Ons', 'add-ons' ),
16730 array( &$this, '_addons_page_render' ),
16731 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Add-Ons', 'add-ons' ),
16732 'manage_options',
16733 'addons',
16734 array( &$this, '_addons_page_load' ),
16735 WP_FS__LOWEST_PRIORITY - 1,
16736 $this->is_submenu_item_visible( 'addons' )
16737 );
16738 }
16739 }
16740
16741 if ( $add_submenu_items ||
16742 ( $is_activation_mode && $this->is_only_premium() && $this->is_admin_page( 'pricing' ) )
16743 ) {
16744 if ( ! WP_FS__DEMO_MODE ) {
16745 $show_pricing = (
16746 $this->is_submenu_item_visible( 'pricing' ) &&
16747 $this->is_pricing_page_visible()
16748 );
16749
16750 $pricing_cta_text = $this->get_pricing_cta_label();
16751 $pricing_class = 'upgrade-mode';
16752 if ( $show_pricing ) {
16753 if ( $this->is_in_trial_promotion() &&
16754 ! $this->is_paying_or_trial()
16755 ) {
16756 // If running a trial promotion, modify the pricing to load the trial.
16757 $pricing_class = 'trial-mode';
16758 } else if ( $this->is_paying() ) {
16759 $pricing_class = '';
16760 }
16761 }
16762
16763 // Add upgrade/pricing page.
16764 $this->add_submenu_item(
16765 $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' ) ),
16766 array( &$this, '_pricing_page_render' ),
16767 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_x_inline( 'Pricing', 'noun', 'pricing' ),
16768 'manage_options',
16769 'pricing',
16770 'Freemius::_clean_admin_content_section',
16771 WP_FS__LOWEST_PRIORITY,
16772 ( $add_submenu_items && $show_pricing ),
16773 $pricing_class
16774 );
16775 }
16776 }
16777
16778 if ( ! $is_activation_mode || ( true !== $this->_storage->require_license_activation ) ) {
16779 /**
16780 * Add the other menu items if there are any when not in activation mode or license activation is not
16781 * required (license activation is required for registered or anonymous users after activating the
16782 * premium version when the site is not in trial mode or there's no active valid license).
16783 *
16784 * @author Leo Fajardo (@leorw)
16785 * @since 2.2.1
16786 */
16787 if ( 0 < count( $this->_menu_items ) ) {
16788 if ( ! $this->_menu->is_top_level() ) {
16789 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
16790
16791 // Append submenu items right after the plugin's submenu item.
16792 $this->order_sub_submenu_items();
16793 } else {
16794 // Append submenu items.
16795 $this->embed_submenu_items();
16796 }
16797 }
16798 }
16799 }
16800
16801 /**
16802 * Moved the actual submenu item additions to a separated function,
16803 * in order to support sub-submenu items when the plugin's settings
16804 * only have a submenu and not top-level menu item.
16805 *
16806 * @author Vova Feldman (@svovaf)
16807 * @since 1.1.4
16808 */
16809 private function embed_submenu_items() {
16810 $item_template = $this->_menu->is_top_level() ?
16811 '<span class="fs-submenu-item %s %s %s">%s</span>' :
16812 '<span class="fs-submenu-item fs-sub %s %s %s">%s</span>';
16813
16814 $top_level_menu_capability = $this->get_top_level_menu_capability();
16815
16816 ksort( $this->_menu_items );
16817
16818 $is_first_submenu_item = true;
16819
16820 foreach ( $this->_menu_items as $priority => $items ) {
16821 foreach ( $items as $item ) {
16822 $capability = ( ! empty( $item['capability'] ) ? $item['capability'] : $top_level_menu_capability );
16823
16824 $menu_item = sprintf(
16825 $item_template,
16826 $this->get_unique_affix(),
16827 $item['menu_slug'],
16828 ! empty( $item['class'] ) ? $item['class'] : '',
16829 $item['menu_title']
16830 );
16831
16832 $top_level_menu_slug = $this->get_top_level_menu_slug();
16833 $menu_slug = $this->_menu->get_slug( $item['menu_slug'] );
16834
16835 if ( ! isset( $item['url'] ) ) {
16836 $hook = FS_Admin_Menu_Manager::add_subpage(
16837 $item['show_submenu'] ?
16838 $top_level_menu_slug :
16839 null,
16840 $item['page_title'],
16841 $menu_item,
16842 $capability,
16843 $menu_slug,
16844 $item['render_function']
16845 );
16846
16847 if ( false !== $item['before_render_function'] ) {
16848 add_action( "load-$hook", $item['before_render_function'] );
16849 }
16850 } else {
16851 FS_Admin_Menu_Manager::add_subpage(
16852 $item['show_submenu'] ?
16853 $top_level_menu_slug :
16854 null,
16855 $item['page_title'],
16856 $menu_item,
16857 $capability,
16858 $menu_slug,
16859 array( $this, '' )
16860 );
16861 }
16862
16863 if ( $item['show_submenu'] && $is_first_submenu_item ) {
16864 if ( $this->_is_network_active && ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
16865 /**
16866 * If the top-level menu has been dynamically created, remove the first submenu item that
16867 * WordPress automatically creates when there's no submenu item whose slug matches the
16868 * parent's. In the following example, the `Awesome Plugin` submenu item will be removed.
16869 *
16870 * Awesome Plugin
16871 * - Awesome Plugin <-- we want to remove this since there's no real setting page for the top-level
16872 *
16873 * @author Leo Fajardo (@leorw)
16874 */
16875 remove_submenu_page( $top_level_menu_slug, $top_level_menu_slug );
16876 }
16877
16878 $is_first_submenu_item = false;
16879 }
16880 }
16881 }
16882 }
16883
16884 /**
16885 * Re-order the submenu items so all Freemius added new submenu items
16886 * are added right after the plugin's settings submenu item.
16887 *
16888 * @author Vova Feldman (@svovaf)
16889 * @since 1.1.4
16890 */
16891 private function order_sub_submenu_items() {
16892 global $submenu;
16893
16894 $menu_slug = $this->_menu->get_top_level_menu_slug();
16895
16896 /**
16897 * Before "admin_menu" fires, WordPress will loop over the default submenus and remove pages for which the user
16898 * does not have permissions. So in case a plugin does not have top-level menu but does have submenus under any
16899 * of the default menus, only users that have the right role can access its sub-submenus (Account, Contact Us,
16900 * Support Forum, etc.) since $submenu[ $menu_slug ] will be empty if the user doesn't have permission.
16901 *
16902 * In case a plugin does not have submenus under any of the default menus but does have submenus under the menu
16903 * of another plugin, only users that have the right role can access its sub-submenus since we will use the
16904 * capability needed to access the parent menu as the capability for the submenus that we will add.
16905 */
16906 if ( empty( $submenu[ $menu_slug ] ) ) {
16907 return;
16908 }
16909
16910 $top_level_menu = &$submenu[ $menu_slug ];
16911
16912 $all_submenu_items_after = array();
16913
16914 $found_submenu_item = false;
16915
16916 foreach ( $top_level_menu as $submenu_id => $meta ) {
16917 if ( $found_submenu_item ) {
16918 // Remove all submenu items after the plugin's submenu item.
16919 $all_submenu_items_after[] = $meta;
16920 unset( $top_level_menu[ $submenu_id ] );
16921 }
16922
16923 if ( $this->_menu->get_raw_slug() === $meta[2] ) {
16924 // Found the submenu item, put all below.
16925 $found_submenu_item = true;
16926 continue;
16927 }
16928 }
16929
16930 // Embed all plugin's new submenu items.
16931 $this->embed_submenu_items();
16932
16933 // Start with specially high number to make sure it's appended.
16934 $i = max( 10000, max( array_keys( $top_level_menu ) ) + 1 );
16935 foreach ( $all_submenu_items_after as $meta ) {
16936 $top_level_menu[ $i ] = $meta;
16937 $i ++;
16938 }
16939
16940 // Sort submenu items.
16941 ksort( $top_level_menu );
16942 }
16943
16944 /**
16945 * Helper method to return the module's support forum URL.
16946 *
16947 * @author Vova Feldman (@svovaf)
16948 * @since 1.2.2.7
16949 *
16950 * @return string
16951 */
16952 function get_support_forum_url() {
16953 return $this->apply_filters( 'support_forum_url', "https://wordpress.org/support/{$this->_module_type}/{$this->_slug}" );
16954 }
16955
16956 /**
16957 * Displays the Support Forum link when enabled.
16958 *
16959 * Can be filtered like so:
16960 *
16961 * function _fs_show_support_menu( $is_visible, $menu_id ) {
16962 * if ( 'support' === $menu_id ) {
16963 * return _fs->is_registered();
16964 * }
16965 * return $is_visible;
16966 * }
16967 * _fs()->add_filter('is_submenu_visible', '_fs_show_support_menu', 10, 2);
16968 *
16969 */
16970 function _add_default_submenu_items() {
16971 if ( ! $this->is_on() ) {
16972 return;
16973 }
16974
16975 if ( ! $this->is_activation_mode() &&
16976 ( ( $this->_is_network_active && fs_is_network_admin() ) ||
16977 ( ! $this->_is_network_active && is_admin() ) )
16978 ) {
16979 $this->add_submenu_link_item(
16980 $this->apply_filters( 'support_forum_submenu', $this->get_text_inline( 'Support Forum', 'support-forum' ) ),
16981 $this->get_support_forum_url(),
16982 'wp-support-forum',
16983 null,
16984 50,
16985 $this->is_submenu_item_visible( 'support' )
16986 );
16987 }
16988 }
16989
16990 /**
16991 * @author Vova Feldman (@svovaf)
16992 * @since 1.0.1
16993 *
16994 * @param string $menu_title
16995 * @param callable $render_function
16996 * @param bool|string $page_title
16997 * @param string $capability
16998 * @param bool|string $menu_slug
16999 * @param bool|callable $before_render_function
17000 * @param int $priority
17001 * @param bool $show_submenu
17002 * @param string $class Since 1.2.1.5 can add custom classes to menu items.
17003 */
17004 function add_submenu_item(
17005 $menu_title,
17006 $render_function,
17007 $page_title = false,
17008 $capability = 'manage_options',
17009 $menu_slug = false,
17010 $before_render_function = false,
17011 $priority = WP_FS__DEFAULT_PRIORITY,
17012 $show_submenu = true,
17013 $class = ''
17014 ) {
17015 $this->_logger->entrance( 'Title = ' . $menu_title );
17016
17017 if ( $this->is_addon() ) {
17018 $parent_fs = $this->get_parent_instance();
17019
17020 if ( is_object( $parent_fs ) ) {
17021 $parent_fs->add_submenu_item(
17022 $menu_title,
17023 $render_function,
17024 $page_title,
17025 $capability,
17026 $menu_slug,
17027 $before_render_function,
17028 $priority,
17029 $show_submenu,
17030 $class
17031 );
17032
17033 return;
17034 }
17035 }
17036
17037 if ( ! isset( $this->_menu_items[ $priority ] ) ) {
17038 $this->_menu_items[ $priority ] = array();
17039 }
17040
17041 $this->_menu_items[ $priority ][] = array(
17042 'page_title' => is_string( $page_title ) ? $page_title : $menu_title,
17043 'menu_title' => $menu_title,
17044 'capability' => $capability,
17045 'menu_slug' => is_string( $menu_slug ) ? $menu_slug : strtolower( $menu_title ),
17046 'render_function' => $render_function,
17047 'before_render_function' => $before_render_function,
17048 'show_submenu' => $show_submenu,
17049 'class' => $class,
17050 );
17051 }
17052
17053 /**
17054 * @author Vova Feldman (@svovaf)
17055 * @since 1.0.1
17056 *
17057 * @param string $menu_title
17058 * @param string $url
17059 * @param bool $menu_slug
17060 * @param string $capability
17061 * @param int $priority
17062 * @param bool $show_submenu
17063 */
17064 function add_submenu_link_item(
17065 $menu_title,
17066 $url,
17067 $menu_slug = false,
17068 $capability = 'read',
17069 $priority = WP_FS__DEFAULT_PRIORITY,
17070 $show_submenu = true
17071 ) {
17072 $this->_logger->entrance( 'Title = ' . $menu_title . '; Url = ' . $url );
17073
17074 if ( $this->is_addon() ) {
17075 $parent_fs = $this->get_parent_instance();
17076
17077 if ( is_object( $parent_fs ) ) {
17078 $parent_fs->add_submenu_link_item(
17079 $menu_title,
17080 $url,
17081 $menu_slug,
17082 $capability,
17083 $priority,
17084 $show_submenu
17085 );
17086
17087 return;
17088 }
17089 }
17090
17091 if ( ! isset( $this->_menu_items[ $priority ] ) ) {
17092 $this->_menu_items[ $priority ] = array();
17093 }
17094
17095 $this->_menu_items[ $priority ][] = array(
17096 'menu_title' => $menu_title,
17097 'capability' => $capability,
17098 'menu_slug' => is_string( $menu_slug ) ? $menu_slug : strtolower( $menu_title ),
17099 'url' => $url,
17100 'page_title' => $menu_title,
17101 'render_function' => 'fs_dummy',
17102 'before_render_function' => '',
17103 'show_submenu' => $show_submenu,
17104 );
17105 }
17106
17107 #endregion ------------------------------------------------------------------
17108
17109
17110 #--------------------------------------------------------------------------------
17111 #region Actions / Hooks / Filters
17112 #--------------------------------------------------------------------------------
17113
17114 /**
17115 * @author Vova Feldman (@svovaf)
17116 * @since 1.1.7
17117 *
17118 * @param string $tag
17119 *
17120 * @return string
17121 */
17122 public function get_action_tag( $tag ) {
17123 return self::get_action_tag_static( $tag, $this->_slug, $this->is_plugin() );
17124 }
17125
17126 /**
17127 * @author Vova Feldman (@svovaf)
17128 * @since 1.2.1.6
17129 *
17130 * @param string $tag
17131 * @param string $slug
17132 * @param bool $is_plugin
17133 *
17134 * @return string
17135 */
17136 static function get_action_tag_static( $tag, $slug = '', $is_plugin = true ) {
17137 $action = "fs_{$tag}";
17138
17139 if ( ! empty( $slug ) ) {
17140 $action .= '_' . self::get_module_unique_affix( $slug, $is_plugin );
17141 }
17142
17143 return $action;
17144 }
17145
17146 /**
17147 * Returns a string that can be used to generate a unique action name,
17148 * option name, HTML element ID, or HTML element class.
17149 *
17150 * @author Leo Fajardo (@leorw)
17151 * @since 1.2.2
17152 *
17153 * @return string
17154 */
17155 public function get_unique_affix() {
17156 return self::get_module_unique_affix( $this->_slug, $this->is_plugin() );
17157 }
17158
17159 /**
17160 * Returns a string that can be used to generate a unique action name,
17161 * option name, HTML element ID, or HTML element class.
17162 *
17163 * @author Vova Feldman (@svovaf)
17164 * @since 1.2.2.5
17165 *
17166 * @param string $slug
17167 * @param bool $is_plugin
17168 *
17169 * @return string
17170 */
17171 static function get_module_unique_affix( $slug, $is_plugin = true ) {
17172 $affix = $slug;
17173
17174 if ( ! $is_plugin ) {
17175 $affix .= '-' . WP_FS__MODULE_TYPE_THEME;
17176 }
17177
17178 return $affix;
17179 }
17180
17181 /**
17182 * @author Vova Feldman (@svovaf)
17183 * @since 1.2.1
17184 * @since 1.2.2.5 The AJAX action names are based on the module ID, not like the non-AJAX actions that are
17185 * based on the slug for backward compatibility.
17186 *
17187 * @param string $tag
17188 *
17189 * @return string
17190 */
17191 function get_ajax_action( $tag ) {
17192 return self::get_ajax_action_static( $tag, $this->_module_id );
17193 }
17194
17195 /**
17196 * @author Vova Feldman (@svovaf)
17197 * @since 1.2.1.7
17198 *
17199 * @param string $tag
17200 *
17201 * @return string
17202 */
17203 function get_ajax_security( $tag ) {
17204 return wp_create_nonce( $this->get_ajax_action( $tag ) );
17205 }
17206
17207 /**
17208 * @author Vova Feldman (@svovaf)
17209 * @since 1.2.1.7
17210 *
17211 * @param string $tag
17212 */
17213 function check_ajax_referer( $tag ) {
17214 check_ajax_referer( $this->get_ajax_action( $tag ), 'security' );
17215 }
17216
17217 /**
17218 * @author Vova Feldman (@svovaf)
17219 * @since 1.2.1.6
17220 * @since 1.2.2.5 The AJAX action names are based on the module ID, not like the non-AJAX actions that are
17221 * based on the slug for backward compatibility.
17222 *
17223 * @param string $tag
17224 * @param number|null $module_id
17225 *
17226 * @return string
17227 */
17228 private static function get_ajax_action_static( $tag, $module_id = null ) {
17229 $action = "fs_{$tag}";
17230
17231 if ( ! empty( $module_id ) ) {
17232 $action .= "_{$module_id}";
17233 }
17234
17235 return $action;
17236 }
17237
17238 /**
17239 * Do action, specific for the current context plugin.
17240 *
17241 * @author Vova Feldman (@svovaf)
17242 * @since 1.0.1
17243 *
17244 * @param string $tag The name of the action to be executed.
17245 * @param mixed $arg,... Optional. Additional arguments which are passed on to the
17246 * functions hooked to the action. Default empty.
17247 *
17248 * @uses do_action()
17249 */
17250 function do_action( $tag, $arg = '' ) {
17251 $this->_logger->entrance( $tag );
17252
17253 $args = func_get_args();
17254
17255 call_user_func_array( 'do_action', array_merge(
17256 array( $this->get_action_tag( $tag ) ),
17257 array_slice( $args, 1 ) )
17258 );
17259 }
17260
17261 /**
17262 * Add action, specific for the current context plugin.
17263 *
17264 * @author Vova Feldman (@svovaf)
17265 * @since 1.0.1
17266 *
17267 * @param string $tag
17268 * @param callable $function_to_add
17269 * @param int $priority
17270 * @param int $accepted_args
17271 *
17272 * @uses add_action()
17273 */
17274 function add_action(
17275 $tag,
17276 $function_to_add,
17277 $priority = WP_FS__DEFAULT_PRIORITY,
17278 $accepted_args = 1
17279 ) {
17280 $this->_logger->entrance( $tag );
17281
17282 add_action( $this->get_action_tag( $tag ), $function_to_add, $priority, $accepted_args );
17283 }
17284
17285 /**
17286 * Add AJAX action, specific for the current context plugin.
17287 *
17288 * @author Vova Feldman (@svovaf)
17289 * @since 1.2.1
17290 *
17291 * @param string $tag
17292 * @param callable $function_to_add
17293 * @param int $priority
17294 *
17295 * @uses add_action()
17296 *
17297 * @return bool True if action added, false if no need to add the action since the AJAX call isn't matching.
17298 */
17299 function add_ajax_action(
17300 $tag,
17301 $function_to_add,
17302 $priority = WP_FS__DEFAULT_PRIORITY
17303 ) {
17304 $this->_logger->entrance( $tag );
17305
17306 return self::add_ajax_action_static(
17307 $tag,
17308 $function_to_add,
17309 $priority,
17310 $this->_module_id
17311 );
17312 }
17313
17314 /**
17315 * Add AJAX action.
17316 *
17317 * @author Vova Feldman (@svovaf)
17318 * @since 1.2.1.6
17319 *
17320 * @param string $tag
17321 * @param callable $function_to_add
17322 * @param int $priority
17323 * @param number|null $module_id
17324 *
17325 * @return bool True if action added, false if no need to add the action since the AJAX call isn't matching.
17326 * @uses add_action()
17327 *
17328 */
17329 static function add_ajax_action_static(
17330 $tag,
17331 $function_to_add,
17332 $priority = WP_FS__DEFAULT_PRIORITY,
17333 $module_id = null
17334 ) {
17335 self::$_static_logger->entrance( $tag );
17336
17337 if ( ! self::is_ajax_action_static( $tag, $module_id ) ) {
17338 return false;
17339 }
17340
17341 add_action(
17342 'wp_ajax_' . self::get_ajax_action_static( $tag, $module_id ),
17343 $function_to_add,
17344 $priority,
17345 0
17346 );
17347
17348 self::$_static_logger->info( "$tag AJAX callback action added." );
17349
17350 return true;
17351 }
17352
17353 /**
17354 * Send a JSON response back to an Ajax request.
17355 *
17356 * @author Vova Feldman (@svovaf)
17357 * @since 1.2.1.5
17358 *
17359 * @param mixed $response
17360 */
17361 static function shoot_ajax_response( $response ) {
17362 wp_send_json( $response );
17363 }
17364
17365 /**
17366 * Send a JSON response back to an Ajax request, indicating success.
17367 *
17368 * @author Vova Feldman (@svovaf)
17369 * @since 1.2.1.5
17370 *
17371 * @param mixed $data Data to encode as JSON, then print and exit.
17372 */
17373 static function shoot_ajax_success( $data = null ) {
17374 wp_send_json_success( $data );
17375 }
17376
17377 /**
17378 * Send a JSON response back to an Ajax request, indicating failure.
17379 *
17380 * @author Vova Feldman (@svovaf)
17381 * @since 1.2.1.5
17382 *
17383 * @param mixed $error Optional error message.
17384 */
17385 static function shoot_ajax_failure( $error = '' ) {
17386 $result = array( 'success' => false );
17387 if ( ! empty( $error ) ) {
17388 $result['error'] = $error;
17389 }
17390
17391 wp_send_json( $result );
17392 }
17393
17394 /**
17395 * Apply filter, specific for the current context plugin.
17396 *
17397 * @author Vova Feldman (@svovaf)
17398 * @since 1.0.9
17399 *
17400 * @param string $tag The name of the filter hook.
17401 * @param mixed $value The value on which the filters hooked to `$tag` are applied on.
17402 *
17403 * @return mixed The filtered value after all hooked functions are applied to it.
17404 *
17405 * @uses apply_filters()
17406 */
17407 function apply_filters( $tag, $value ) {
17408 $this->_logger->entrance( $tag );
17409
17410 $args = func_get_args();
17411 array_unshift( $args, $this->get_unique_affix() );
17412
17413 return call_user_func_array( 'fs_apply_filter', $args );
17414 }
17415
17416 /**
17417 * Add filter, specific for the current context plugin.
17418 *
17419 * @author Vova Feldman (@svovaf)
17420 * @since 1.0.9
17421 *
17422 * @param string $tag
17423 * @param callable $function_to_add
17424 * @param int $priority
17425 * @param int $accepted_args
17426 *
17427 * @uses add_filter()
17428 */
17429 function add_filter( $tag, $function_to_add, $priority = WP_FS__DEFAULT_PRIORITY, $accepted_args = 1 ) {
17430 $this->_logger->entrance( $tag );
17431
17432 add_filter( $this->get_action_tag( $tag ), $function_to_add, $priority, $accepted_args );
17433 }
17434
17435 /**
17436 * Check if has filter.
17437 *
17438 * @author Vova Feldman (@svovaf)
17439 * @since 1.1.4
17440 *
17441 * @param string $tag
17442 * @param callable|bool $function_to_check Optional. The callback to check for. Default false.
17443 *
17444 * @return false|int
17445 *
17446 * @uses has_filter()
17447 */
17448 function has_filter( $tag, $function_to_check = false ) {
17449 $this->_logger->entrance( $tag );
17450
17451 return has_filter( $this->get_action_tag( $tag ), $function_to_check );
17452 }
17453
17454 #endregion
17455
17456 /**
17457 * Override default i18n text phrases.
17458 *
17459 * @author Vova Feldman (@svovaf)
17460 * @since 1.1.6
17461 *
17462 * @param string[] string $key_value
17463 *
17464 * @uses fs_override_i18n()
17465 */
17466 function override_i18n( $key_value ) {
17467 fs_override_i18n( $key_value, $this->_slug );
17468 }
17469
17470 /* Account Page
17471 ------------------------------------------------------------------------------------------------------------------*/
17472 /**
17473 * Update site information.
17474 *
17475 * @author Vova Feldman (@svovaf)
17476 * @since 1.0.1
17477 *
17478 * @param bool $store Flush to Database if true.
17479 * @param null|int $network_level_or_blog_id Since 2.0.0
17480 * @param \FS_Site $site Since 2.0.0
17481 */
17482 private function _store_site( $store = true, $network_level_or_blog_id = null, FS_Site $site = null ) {
17483 $this->_logger->entrance();
17484
17485 if ( empty( $this->_site->id ) ) {
17486 $this->_logger->error( "Empty install ID, can't store site." );
17487
17488 return;
17489 }
17490
17491 $site_clone = is_object( $site ) ? $site : $this->_site;
17492 $encrypted_site = clone $site_clone;
17493
17494 $sites = self::get_all_sites( $this->_module_type, $network_level_or_blog_id );
17495
17496 $prev_stored_user_id = $this->_storage->get( 'prev_user_id', false, $network_level_or_blog_id );
17497
17498 if ( empty( $prev_stored_user_id ) &&
17499 $this->_user->id != $this->_site->user_id
17500 ) {
17501 /**
17502 * Store the current user ID as the previous user ID so that the previous user can be used
17503 * as the install's owner while the new owner's details are not yet available.
17504 *
17505 * This will be executed only in the `replica` site. For example, there are 2 sites, namely `original`
17506 * and `replica`, then an ownership change was initiated and completed in the `original`, the `replica`
17507 * will be using the previous user until it is updated again (e.g.: until the next clone of `original`
17508 * into `replica`.
17509 *
17510 * @author Leo Fajardo (@leorw)
17511 */
17512 $this->_storage->store( 'prev_user_id', $sites[ $this->_slug ]->user_id, $network_level_or_blog_id );
17513 }
17514
17515 $sites[ $this->_slug ] = $encrypted_site;
17516
17517 $this->set_account_option( 'sites', $sites, $store, $network_level_or_blog_id );
17518 }
17519
17520 /**
17521 * Update plugin's plans information.
17522 *
17523 * @author Vova Feldman (@svovaf)
17524 * @since 1.0.2
17525 *
17526 * @param bool $store Flush to Database if true.
17527 */
17528 private function _store_plans( $store = true ) {
17529 $this->_logger->entrance();
17530
17531 $plans = self::get_all_plans( $this->_module_type );
17532
17533 // Copy plans.
17534 $encrypted_plans = array();
17535 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
17536 $encrypted_plans[] = self::_encrypt_entity( $this->_plans[ $i ] );
17537 }
17538
17539 $plans[ $this->_slug ] = $encrypted_plans;
17540
17541 $this->set_account_option( 'plans', $plans, $store );
17542 }
17543
17544 /**
17545 * Update user's plugin licenses.
17546 *
17547 * @author Vova Feldman (@svovaf)
17548 * @since 1.0.5
17549 *
17550 * @param bool $store
17551 * @param number|bool $module_id
17552 * @param FS_Plugin_License[] $licenses
17553 */
17554 private function _store_licenses( $store = true, $module_id = false, $licenses = array() ) {
17555 $this->_logger->entrance();
17556
17557 $all_licenses = self::get_all_licenses();
17558
17559 if ( ! FS_Plugin::is_valid_id( $module_id ) ) {
17560 $module_id = $this->_module_id;
17561
17562 $user_licenses = is_array( $this->_licenses ) ?
17563 $this->_licenses :
17564 array();
17565
17566 if ( empty( $user_licenses ) ) {
17567 // If the context user doesn't have any license, don't update the licenses collection.
17568 return;
17569 }
17570
17571 $new_user_licenses_map = array();
17572 foreach ( $user_licenses as $user_license ) {
17573 $new_user_licenses_map[ $user_license->id ] = $user_license;
17574 }
17575
17576 self::store_user_id_license_ids_map( array_keys( $new_user_licenses_map ), $this->_module_id, $this->_user->id );
17577
17578 // Update user licenses.
17579 $licenses_to_update_count = count( $new_user_licenses_map );
17580 foreach ( $all_licenses[ $module_id ] as $key => $license ) {
17581 if ( 0 === $licenses_to_update_count ) {
17582 break;
17583 }
17584
17585 if ( isset( $new_user_licenses_map[ $license->id ] ) ) {
17586 // Update license.
17587 $all_licenses[ $module_id ][ $key ] = $new_user_licenses_map[ $license->id ];
17588 unset( $new_user_licenses_map[ $license->id ] );
17589
17590 $licenses_to_update_count --;
17591 }
17592 }
17593
17594 if ( ! empty( $new_user_licenses_map ) ) {
17595 // Add new licenses.
17596 $all_licenses[ $module_id ] = array_merge( array_values( $new_user_licenses_map ), $all_licenses[ $module_id ] );
17597 }
17598
17599 $licenses = $all_licenses[ $module_id ];
17600 }
17601
17602 if ( ! isset( $all_licenses[ $module_id ] ) ) {
17603 $all_licenses[ $module_id ] = array();
17604 }
17605
17606 $all_licenses[ $module_id ] = $licenses;
17607
17608 self::$_accounts->set_option( 'all_licenses', $all_licenses, $store );
17609 }
17610
17611 /**
17612 * Update user information.
17613 *
17614 * @author Vova Feldman (@svovaf)
17615 * @since 1.0.1
17616 *
17617 * @param bool $store Flush to Database if true.
17618 */
17619 private function _store_user( $store = true ) {
17620 $this->_logger->entrance();
17621
17622 if ( empty( $this->_user->id ) ) {
17623 $this->_logger->error( "Empty user ID, can't store user." );
17624
17625 return;
17626 }
17627
17628 $users = self::get_all_users();
17629 $users[ $this->_user->id ] = $this->_user;
17630 self::$_accounts->set_option( 'users', $users, $store );
17631 }
17632
17633 /**
17634 * Update new updates information.
17635 *
17636 * @author Vova Feldman (@svovaf)
17637 * @since 1.0.4
17638 *
17639 * @param FS_Plugin_Tag|null $update
17640 * @param bool $store Flush to Database if true.
17641 * @param bool|number $plugin_id
17642 */
17643 private function _store_update( $update, $store = true, $plugin_id = false ) {
17644 $this->_logger->entrance();
17645
17646 if ( $update instanceof FS_Plugin_Tag ) {
17647 $update->updated = time();
17648 }
17649
17650 if ( ! is_numeric( $plugin_id ) ) {
17651 $plugin_id = $this->_plugin->id;
17652 }
17653
17654 $updates = self::get_all_updates();
17655 $updates[ $plugin_id ] = $update;
17656 self::$_accounts->set_option( 'updates', $updates, $store );
17657 }
17658
17659 /**
17660 * Update new updates information.
17661 *
17662 * @author Vova Feldman (@svovaf)
17663 * @since 1.0.6
17664 *
17665 * @param FS_Plugin[] $plugin_addons
17666 * @param bool $store Flush to Database if true.
17667 */
17668 private function _store_addons( $plugin_addons, $store = true ) {
17669 $this->_logger->entrance();
17670
17671 $addons = self::get_all_addons();
17672 $addons[ $this->_plugin->id ] = $plugin_addons;
17673 self::$_accounts->set_option( 'addons', $addons, $store );
17674 }
17675
17676 /**
17677 * Delete plugin's associated add-ons.
17678 *
17679 * @author Vova Feldman (@svovaf)
17680 * @since 1.0.8
17681 *
17682 * @param bool $store
17683 *
17684 * @return bool
17685 */
17686 private function _delete_account_addons( $store = true ) {
17687 $all_addons = self::get_all_account_addons();
17688
17689 if ( ! isset( $all_addons[ $this->_plugin->id ] ) ) {
17690 return false;
17691 }
17692
17693 unset( $all_addons[ $this->_plugin->id ] );
17694
17695 self::$_accounts->set_option( 'account_addons', $all_addons, $store );
17696
17697 return true;
17698 }
17699
17700 /**
17701 * Update account add-ons list.
17702 *
17703 * @author Vova Feldman (@svovaf)
17704 * @since 1.0.6
17705 *
17706 * @param FS_Plugin[] $addons
17707 * @param bool $store Flush to Database if true.
17708 */
17709 private function _store_account_addons( $addons, $store = true ) {
17710 $this->_logger->entrance();
17711
17712 $all_addons = self::get_all_account_addons();
17713 $all_addons[ $this->_plugin->id ] = $addons;
17714 self::$_accounts->set_option( 'account_addons', $all_addons, $store );
17715 }
17716
17717 /**
17718 * Purges the cache for the valid user licenses API call so that when the `Account` or `Add-Ons` page is loaded,
17719 * the valid user licenses will be fetched again and the account add-ons may be updated.
17720 *
17721 * @author Leo Fajardo (@leorw)
17722 * @since 2.2.4
17723 */
17724 private function purge_valid_user_licenses_cache() {
17725 $this->get_api_user_scope()->purge_cache( $this->get_valid_user_licenses_endpoint() );
17726 }
17727
17728 /**
17729 * @author Leo Fajardo (@leorw)
17730 * @since 2.3.0
17731 *
17732 * @param array $all_licenses
17733 * @param number|null $site_license_id
17734 * @param bool $include_parent_licenses
17735 *
17736 * @return array
17737 */
17738 private function get_foreign_licenses_info( $all_licenses, $site_license_id = null, $include_parent_licenses = false ) {
17739 $foreign_licenses = array(
17740 'ids' => array(),
17741 'license_keys' => array()
17742 );
17743
17744 $parent_license_ids_map = array();
17745
17746 foreach ( $all_licenses as $license ) {
17747 if ( $license->user_id == $this->_user->id || $license->id == $site_license_id ) {
17748 continue;
17749 }
17750
17751 $foreign_licenses['ids'][] = $license->id;
17752 $foreign_licenses['license_keys'][] = $license->secret_key;
17753
17754 if (
17755 $include_parent_licenses &&
17756 is_object( $this->_license ) &&
17757 FS_Plugin_License::is_valid_id( $this->_license->parent_license_id ) &&
17758 ! isset( $parent_license_ids_map[ $this->_license->parent_license_id ] )
17759 ) {
17760 /**
17761 * Include the parent license's info only if it has not been included before since child licenses
17762 * can have the same parent license.
17763 */
17764 $foreign_licenses['ids'][] = $this->_license->parent_license_id;
17765 $foreign_licenses['license_keys'][] = $license->secret_key;
17766
17767 $parent_license_ids_map[ $this->_license->parent_license_id ] = true;
17768 }
17769 }
17770
17771 if ( empty( $foreign_licenses['ids'] ) ) {
17772 $foreign_licenses = array();
17773 }
17774
17775 return $foreign_licenses;
17776 }
17777
17778 /**
17779 * @author Leo Fajardo (@leorw)
17780 * @since 2.3.0
17781 *
17782 * @return string
17783 */
17784 private function get_valid_user_licenses_endpoint() {
17785 $user_licenses_endpoint = '/licenses.json?type=active' .
17786 ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? '&is_enriched=true' : '' );
17787
17788 $foreign_licenses = $this->get_foreign_licenses_info( self::get_all_licenses( $this->_module_id ), null, true );
17789
17790 if ( ! empty ( $foreign_licenses ) ) {
17791 $foreign_licenses = array(
17792 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
17793 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
17794 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
17795 );
17796
17797 $user_licenses_endpoint = add_query_arg( $foreign_licenses, $user_licenses_endpoint );
17798 }
17799
17800 return $user_licenses_endpoint;
17801 }
17802
17803 /**
17804 * Fetches active licenses that are enriched with product type if there's a context `bundle_id` and bundle
17805 * licenses enriched with product IDs if there are any. From the licenses, the `get_updated_account_addons`
17806 * method filters out non–add-on product IDs and stores the add-on IDs.
17807 *
17808 * @author Leo Fajardo (@leorw)
17809 * @since 2.2.4
17810 *
17811 * @return stdClass[] array
17812 */
17813 private function fetch_valid_user_licenses() {
17814 $this->_logger->entrance();
17815
17816 $result = $this->get_api_user_scope()->get( $this->get_valid_user_licenses_endpoint() );
17817
17818 if ( ! $this->is_api_result_object( $result, 'licenses' ) ||
17819 ! is_array( $result->licenses )
17820 ) {
17821 return array();
17822 }
17823
17824 return $result->licenses;
17825 }
17826
17827 /**
17828 * @author Leo Fajardo (@leorw)
17829 * @since 2.2.4
17830 *
17831 * @return number[] Account add-on IDs.
17832 */
17833 function get_updated_account_addons() {
17834 $addons = $this->get_addons();
17835 if ( empty( $addons ) ) {
17836 return array();
17837 }
17838
17839 $account_addons = $this->get_account_addons();
17840 if ( ! is_array( $account_addons ) ) {
17841 $account_addons = array();
17842 }
17843
17844 $user_licenses = $this->is_registered() ?
17845 $this->fetch_valid_user_licenses() :
17846 array();
17847
17848 if ( empty( $user_licenses ) ) {
17849 return $account_addons;
17850 }
17851
17852 $addon_ids = array();
17853 foreach ( $addons as $addon ) {
17854 $addon_ids[] = $addon->id;
17855 }
17856
17857 $license_product_ids = array();
17858
17859 foreach ( $user_licenses as $license ) {
17860 if ( isset( $license->plugin_type ) && 'bundle' === $license->plugin_type ) {
17861 $license_product_ids = array_merge( $license_product_ids, $license->products );
17862 } else {
17863 $license_product_ids[] = $license->plugin_id;
17864 }
17865 }
17866
17867 // Filter out non–add-on IDs.
17868 $new_account_addons = array_intersect( $addon_ids, $license_product_ids );
17869 if ( count( $new_account_addons ) !== count( $account_addons ) ) {
17870 $this->_store_account_addons( array_unique( $new_account_addons ) );
17871 }
17872
17873 return $new_account_addons;
17874 }
17875
17876 /**
17877 * Store account params in the Database.
17878 *
17879 * @author Vova Feldman (@svovaf)
17880 * @since 1.0.1
17881 *
17882 * @param null|int $blog_id Since 2.0.0
17883 */
17884 private function _store_account( $blog_id = null ) {
17885 $this->_logger->entrance();
17886
17887 $this->_store_site( false, $blog_id );
17888 $this->_store_user( false );
17889 $this->_store_plans( false );
17890 $this->_store_licenses( false );
17891
17892 self::$_accounts->store( $blog_id );
17893 }
17894
17895 /**
17896 * Sync user's information.
17897 *
17898 * @author Vova Feldman (@svovaf)
17899 * @since 1.0.3
17900 * @uses FS_Api
17901 */
17902 private function _handle_account_user_sync() {
17903 $this->_logger->entrance();
17904
17905 $api = $this->get_api_user_scope();
17906
17907 // Get user's information.
17908 $user = $api->get( '/', true );
17909
17910 if ( isset( $user->id ) ) {
17911 $this->_user->first = $user->first;
17912 $this->_user->last = $user->last;
17913 $this->_user->email = $user->email;
17914
17915 $is_menu_item_account_visible = $this->is_submenu_item_visible( 'account' );
17916
17917 if ( $user->is_verified &&
17918 ( ! isset( $this->_user->is_verified ) || false === $this->_user->is_verified )
17919 ) {
17920 $this->_user->is_verified = true;
17921
17922 $this->do_action( 'account_email_verified', $user->email );
17923
17924 $this->_admin_notices->add(
17925 $this->get_text_inline( 'Your email has been successfully verified - you are AWESOME!', 'email-verified-message' ),
17926 $this->get_text_x_inline( 'Right on', 'a positive response', 'right-on' ) . '!',
17927 'success',
17928 // Make admin sticky if account menu item is invisible,
17929 // since the page will be auto redirected to the plugin's
17930 // main settings page, and the non-sticky message
17931 // will disappear.
17932 ! $is_menu_item_account_visible,
17933 'email_verified'
17934 );
17935 }
17936
17937 // Flush user details to DB.
17938 $this->_store_user();
17939
17940 $this->do_action( 'after_account_user_sync', $user );
17941
17942 /**
17943 * If account menu item is hidden, redirect to plugin's main settings page.
17944 *
17945 * @author Vova Feldman (@svovaf)
17946 * @since 1.1.6
17947 *
17948 * @link https://github.com/Freemius/wordpress-sdk/issues/6
17949 */
17950 if ( ! $is_menu_item_account_visible ) {
17951 fs_redirect( $this->_get_admin_page_url() );
17952 }
17953 }
17954 }
17955
17956 /**
17957 * @author Vova Feldman (@svovaf)
17958 * @since 1.0.9
17959 * @uses FS_Api
17960 *
17961 * @param number|bool $license_id
17962 *
17963 * @return FS_Subscription|object|bool
17964 */
17965 private function _fetch_site_license_subscription( $license_id = false ) {
17966 $this->_logger->entrance();
17967 $api = $this->get_api_site_scope();
17968
17969 if ( ! is_numeric( $license_id ) ) {
17970 $license_id = FS_Plugin_License::is_valid_id( $this->_license->parent_license_id ) ?
17971 $this->_license->parent_license_id :
17972 $this->_license->id;
17973 }
17974
17975 $result = $api->get( "/licenses/{$license_id}/subscriptions.json", true );
17976
17977 return ! isset( $result->error ) ?
17978 ( ( is_array( $result->subscriptions ) && 0 < count( $result->subscriptions ) ) ?
17979 new FS_Subscription( $result->subscriptions[0] ) :
17980 false
17981 ) :
17982 $result;
17983 }
17984
17985 /**
17986 * @author Vova Feldman (@svovaf)
17987 * @since 1.0.4
17988 * @uses FS_Api
17989 *
17990 * @param number|bool $plan_id
17991 *
17992 * @return FS_Plugin_Plan|object
17993 */
17994 private function _fetch_site_plan( $plan_id = false ) {
17995 $this->_logger->entrance();
17996 $api = $this->get_api_site_scope();
17997
17998 if ( ! is_numeric( $plan_id ) ) {
17999 $plan_id = $this->_site->plan_id;
18000 }
18001
18002 $plan = $api->get( "/plans/{$plan_id}.json", true );
18003
18004 return ! isset( $plan->error ) ? new FS_Plugin_Plan( $plan ) : $plan;
18005 }
18006
18007 /**
18008 * @author Vova Feldman (@svovaf)
18009 * @since 1.0.5
18010 * @uses FS_Api
18011 *
18012 * @return FS_Plugin_Plan[]|object
18013 */
18014 private function _fetch_plugin_plans() {
18015 $this->_logger->entrance();
18016 $api = $this->get_current_or_network_user_api_scope();
18017
18018 /**
18019 * @since 1.2.3 When running in DEV mode, retrieve pending plans as well.
18020 */
18021 $result = $api->get( $this->add_show_pending( "/plugins/{$this->_module_id}/plans.json" ), true );
18022
18023 if ( $this->is_api_result_object( $result, 'plans' ) && is_array( $result->plans ) ) {
18024 for ( $i = 0, $len = count( $result->plans ); $i < $len; $i ++ ) {
18025 $result->plans[ $i ] = new FS_Plugin_Plan( $result->plans[ $i ] );
18026 }
18027
18028 $result = $result->plans;
18029 }
18030
18031 return $result;
18032 }
18033
18034 /**
18035 * @author Vova Feldman (@svovaf)
18036 * @since 2.0.0
18037 *
18038 * @param number $plan_id
18039 *
18040 * @return \FS_Plugin_Plan|object
18041 */
18042 private function fetch_plan_by_id( $plan_id ) {
18043 $this->_logger->entrance();
18044 $api = $this->get_current_or_network_user_api_scope();
18045
18046 $result = $api->get( "/plugins/{$this->_module_id}/plans/{$plan_id}.json", true );
18047
18048 return $this->is_api_result_entity( $result ) ?
18049 new FS_Plugin_Plan( $result ) :
18050 $result;
18051 }
18052
18053 /**
18054 * @author Vova Feldman (@svovaf)
18055 * @since 1.0.5
18056 * @uses FS_Api
18057 *
18058 * @param number|bool $plugin_id
18059 * @param number|bool $site_license_id
18060 * @param array $foreign_licenses @since 2.0.0. This is used by network-activated plugins.
18061 * @param number|null $blog_id
18062 *
18063 * @return FS_Plugin_License[]|object
18064 */
18065 private function _fetch_licenses(
18066 $plugin_id = false,
18067 $site_license_id = false,
18068 $foreign_licenses = array(),
18069 $blog_id = null
18070 ) {
18071 $this->_logger->entrance();
18072
18073 $api = $this->get_api_user_scope();
18074
18075 if ( ! is_numeric( $plugin_id ) ) {
18076 $plugin_id = $this->_plugin->id;
18077 }
18078
18079 $user_licenses_endpoint = "/plugins/{$plugin_id}/licenses.json?is_enriched=true";
18080 if ( ! empty ( $foreign_licenses ) ) {
18081 $foreign_licenses = array(
18082 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
18083 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
18084 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
18085 );
18086
18087 $user_licenses_endpoint = add_query_arg( $foreign_licenses, $user_licenses_endpoint );
18088 }
18089
18090 $result = $api->get( $user_licenses_endpoint, true );
18091
18092 $is_site_license_synced = false;
18093
18094 $api_errors = array();
18095
18096 if ( $this->is_api_result_object( $result, 'licenses' ) &&
18097 is_array( $result->licenses )
18098 ) {
18099 for ( $i = 0, $len = count( $result->licenses ); $i < $len; $i ++ ) {
18100 $result->licenses[ $i ] = new FS_Plugin_License( $result->licenses[ $i ] );
18101
18102 if ( ( ! $is_site_license_synced ) && is_numeric( $site_license_id ) ) {
18103 $is_site_license_synced = ( $site_license_id == $result->licenses[ $i ]->id );
18104 }
18105 }
18106
18107 $result = $result->licenses;
18108 } else {
18109 $api_errors[] = $result;
18110 $result = array();
18111 }
18112
18113 if ( ! $is_site_license_synced ) {
18114 if ( ! is_null( $blog_id ) ) {
18115 /**
18116 * If blog ID is not null, the request is for syncing of the license of a single site via the
18117 * network-level "Account" page.
18118 *
18119 * @author Leo Fajardo (@leorw)
18120 */
18121 $this->switch_to_blog( $blog_id );
18122 }
18123
18124 $api = $this->get_api_site_scope();
18125
18126 if ( is_numeric( $site_license_id ) ) {
18127 // Try to retrieve a foreign license that is linked to the install.
18128 $api_result = $api->call( '/licenses.json?is_enriched=true' );
18129
18130 if ( $this->is_api_result_object( $api_result, 'licenses' ) &&
18131 is_array( $api_result->licenses )
18132 ) {
18133 $licenses = $api_result->licenses;
18134
18135 if ( ! empty( $licenses ) ) {
18136 $result[] = new FS_Plugin_License( $licenses[0] );
18137 }
18138 } else {
18139 $api_errors[] = $api_result;
18140 }
18141 } else if (
18142 is_object( $this->_license ) &&
18143 /**
18144 * Sync only if the license belongs to the context plugin. `$plugin_id` can be an add-on ID while
18145 * the FS instance that does the syncing is the parent FS instance.
18146 *
18147 * @author Leo Fajardo (@leorw)
18148 * @since 2.3.0
18149 */
18150 $this->_license->plugin_id == $plugin_id
18151 ) {
18152 $is_license_in_result = false;
18153 if ( ! empty( $result ) ) {
18154 foreach ( $result as $license ) {
18155 if ( $license->id == $this->_license->id ) {
18156 $is_license_in_result = true;
18157 break;
18158 }
18159 }
18160 }
18161
18162 if ( ! $is_license_in_result ) {
18163 // Fetch foreign license by ID and license key.
18164 $license = $api->get( "/licenses/{$this->_license->id}.json?license_key=" .
18165 urlencode( $this->_license->secret_key ) . '&is_enriched=true' );
18166
18167 if ( $this->is_api_result_entity( $license ) ) {
18168 $result[] = new FS_Plugin_License( $license );
18169 } else {
18170 $api_errors[] = $license;
18171 }
18172 }
18173 }
18174
18175 if ( ! is_null( $blog_id ) ) {
18176 $this->switch_to_blog( $this->_storage->network_install_blog_id );
18177 }
18178 }
18179
18180 if ( is_array( $result ) && 0 < count( $result ) ) {
18181 // If found at least one license, return license collection even if there are errors.
18182 return $result;
18183 }
18184
18185 if ( ! empty( $api_errors ) ) {
18186 // If found any errors and no licenses, return first error.
18187 return $api_errors[0];
18188 }
18189
18190 // Fallback to empty licenses list.
18191 return $result;
18192 }
18193
18194 /**
18195 * @author Vova Feldman (@svovaf)
18196 * @since 2.0.0
18197 *
18198 * @param number $license_id
18199 * @param string $license_key
18200 *
18201 * @return \FS_Plugin_License|object
18202 */
18203 private function fetch_license_by_key( $license_id, $license_key ) {
18204 $this->_logger->entrance();
18205
18206 $api = $this->get_current_or_network_user_api_scope();
18207
18208 $result = $api->get( "/licenses/{$license_id}.json?license_key=" . urlencode( $license_key ) );
18209
18210 return $this->is_api_result_entity( $result ) ?
18211 new FS_Plugin_License( $result ) :
18212 $result;
18213 }
18214
18215 /**
18216 * @author Vova Feldman (@svovaf)
18217 * @since 1.2.0
18218 * @uses FS_Api
18219 *
18220 * @param number|bool $plugin_id
18221 * @param bool $flush
18222 *
18223 * @return FS_Payment[]|object
18224 */
18225 function _fetch_payments( $plugin_id = false, $flush = false ) {
18226 $this->_logger->entrance();
18227
18228 $api = $this->get_api_user_scope();
18229
18230 if ( ! is_numeric( $plugin_id ) ) {
18231 $plugin_id = $this->_plugin->id;
18232 }
18233
18234 $include_bundles = (
18235 is_object( $this->_plugin ) &&
18236 FS_Plugin::is_valid_id( $this->_plugin->bundle_id )
18237 );
18238
18239 $result = $api->get(
18240 "/plugins/{$plugin_id}/payments.json?include_addons=true" . ($include_bundles ? '&include_bundles=true' : ''),
18241 $flush
18242 );
18243
18244 if ( ! isset( $result->error ) ) {
18245 for ( $i = 0, $len = count( $result->payments ); $i < $len; $i ++ ) {
18246 $result->payments[ $i ] = new FS_Payment( $result->payments[ $i ] );
18247 }
18248 $result = $result->payments;
18249 }
18250
18251 return $result;
18252 }
18253
18254 /**
18255 * @author Vova Feldman (@svovaf)
18256 * @since 1.2.1.5
18257 * @uses FS_Api
18258 *
18259 * @param bool $flush
18260 *
18261 * @return \FS_Billing|mixed
18262 */
18263 function _fetch_billing( $flush = false ) {
18264 require_once WP_FS__DIR_INCLUDES . '/entities/class-fs-billing.php';
18265
18266 $billing = $this->get_api_user_scope()->get( 'billing.json', $flush );
18267
18268 if ( $this->is_api_result_entity( $billing ) ) {
18269 $billing = new FS_Billing( $billing );
18270 }
18271
18272 return $billing;
18273 }
18274
18275 /**
18276 * @author Vova Feldman (@svovaf)
18277 * @since 1.0.5
18278 *
18279 * @param FS_Plugin_License[] $licenses
18280 * @param number $module_id
18281 */
18282 private function _update_licenses( $licenses, $module_id ) {
18283 $this->_logger->entrance();
18284
18285 if ( is_array( $licenses ) ) {
18286 for ( $i = 0, $len = count( $licenses ); $i < $len; $i ++ ) {
18287 $licenses[ $i ]->updated = time();
18288 }
18289 }
18290
18291 $this->_store_licenses( true, $module_id, $licenses );
18292 }
18293
18294 /**
18295 * @author Vova Feldman (@svovaf)
18296 * @since 1.0.4
18297 *
18298 * @param bool|number $plugin_id
18299 * @param bool $flush Since 1.1.7.3
18300 * @param int $expiration Since 1.2.2.7
18301 * @param bool|string $newer_than Since 2.2.1
18302 *
18303 * @return object|false New plugin tag info if exist.
18304 */
18305 private function _fetch_newer_version( $plugin_id = false, $flush = true, $expiration = WP_FS__TIME_24_HOURS_IN_SEC, $newer_than = false ) {
18306 $latest_tag = $this->_fetch_latest_version( $plugin_id, $flush, $expiration, $newer_than );
18307
18308 if ( ! is_object( $latest_tag ) ) {
18309 return false;
18310 }
18311
18312 $plugin_version = $this->get_plugin_version();
18313
18314 // Check if version is actually newer.
18315 $has_new_version =
18316 // If it's an non-installed add-on then always return latest.
18317 ( $this->_is_addon_id( $plugin_id ) && ! $this->is_addon_activated( $plugin_id ) ) ||
18318 // Compare versions.
18319 version_compare( $plugin_version, $latest_tag->version, '<' );
18320
18321 $this->_logger->departure( $has_new_version ? 'Found newer plugin version ' . $latest_tag->version : 'No new version' );
18322
18323 $is_latest_version_beta = ( 'beta' === $latest_tag->release_mode );
18324
18325 $this->_storage->beta_data = array(
18326 'is_beta' => $is_latest_version_beta,
18327 'version' => $latest_tag->version
18328 );
18329
18330 return $has_new_version ? $latest_tag : false;
18331 }
18332
18333 /**
18334 * @author Vova Feldman (@svovaf)
18335 * @since 1.0.5
18336 *
18337 * @param bool|number $plugin_id
18338 * @param bool $flush Since 1.1.7.3
18339 * @param int $expiration Since 1.2.2.7
18340 * @param bool|string $newer_than Since 2.2.1
18341 *
18342 * @return bool|FS_Plugin_Tag
18343 */
18344 function get_update( $plugin_id = false, $flush = true, $expiration = WP_FS__TIME_24_HOURS_IN_SEC, $newer_than = false ) {
18345 $this->_logger->entrance();
18346
18347 if ( ! is_numeric( $plugin_id ) ) {
18348 $plugin_id = $this->_plugin->id;
18349 }
18350
18351 $this->check_updates( true, $plugin_id, $flush, $expiration, $newer_than );
18352 $updates = $this->get_all_updates();
18353
18354 return isset( $updates[ $plugin_id ] ) && is_object( $updates[ $plugin_id ] ) ? $updates[ $plugin_id ] : false;
18355 }
18356
18357 /**
18358 * Check if site assigned with active license.
18359 *
18360 * @author Vova Feldman (@svovaf)
18361 * @since 1.0.6
18362 *
18363 * @deprecated Please use has_active_valid_license() instead because license can be cancelled.
18364 */
18365 function has_active_license() {
18366 return (
18367 is_object( $this->_license ) &&
18368 is_numeric( $this->_license->id ) &&
18369 ! $this->_license->is_expired()
18370 );
18371 }
18372
18373 /**
18374 * Check if site assigned with active & valid (not expired) license.
18375 *
18376 * @author Vova Feldman (@svovaf)
18377 * @since 1.2.1
18378 */
18379 function has_active_valid_license() {
18380 return self::is_active_valid_license( $this->_license );
18381 }
18382
18383 /**
18384 * Check if a given license is active & valid (not expired).
18385 *
18386 * @author Vova Feldman (@svovaf)
18387 * @since 2.1.3
18388 *
18389 * @param FS_Plugin_License $license
18390 *
18391 * @return bool
18392 */
18393 private static function is_active_valid_license( $license ) {
18394 return (
18395 is_object( $license ) &&
18396 FS_Plugin_License::is_valid_id( $license->id ) &&
18397 $license->is_active() &&
18398 $license->is_valid()
18399 );
18400 }
18401
18402 /**
18403 * Checks if there's any site that is associated with an active & valid license.
18404 * This logic is used to determine if the admin can download the premium code base from a network level admin.
18405 *
18406 * @author Vova Feldman (@svovaf)
18407 * @since 2.1.3
18408 *
18409 * @return bool
18410 */
18411 function has_any_active_valid_license() {
18412 if ( ! fs_is_network_admin() ) {
18413 return $this->has_active_valid_license();
18414 }
18415
18416 $installs = $this->get_blog_install_map();
18417 $all_plugin_licenses = self::get_all_licenses( $this->_module_id );
18418
18419 foreach ( $installs as $blog_id => $install ) {
18420 if ( ! FS_Plugin_License::is_valid_id( $install->license_id ) ) {
18421 continue;
18422 }
18423
18424 foreach ( $all_plugin_licenses as $license ) {
18425 if ( $license->id == $install->license_id ) {
18426 if ( self::is_active_valid_license( $license ) ) {
18427 return true;
18428 }
18429 }
18430 }
18431 }
18432
18433 return false;
18434 }
18435
18436 /**
18437 * Check if site assigned with license with enabled features.
18438 *
18439 * @author Vova Feldman (@svovaf)
18440 * @since 1.0.6
18441 *
18442 * @return bool
18443 */
18444 function has_features_enabled_license() {
18445 return (
18446 is_object( $this->_license ) &&
18447 is_numeric( $this->_license->id ) &&
18448 $this->_license->is_features_enabled()
18449 );
18450 }
18451
18452 /**
18453 * Check if user is a trial or have feature enabled license.
18454 *
18455 * @author Vova Feldman (@svovaf)
18456 * @since 1.1.7
18457 *
18458 * @return bool
18459 */
18460 function can_use_premium_code() {
18461 return $this->is_trial() || $this->has_features_enabled_license();
18462 }
18463
18464 /**
18465 * Checks if the current user can activate plugins or switch themes. Note that this method should only be used
18466 * after the `init` action is triggered because it is using `current_user_can()` which is only functional after
18467 * the context user is authenticated.
18468 *
18469 * @author Leo Fajardo (@leorw)
18470 * @since 1.2.2
18471 *
18472 * @return bool
18473 */
18474 function is_user_admin() {
18475 /**
18476 * Require a super-admin when network activated, running from the network level OR if
18477 * running from the site level but not delegated the opt-in.
18478 *
18479 * @author Vova Feldman (@svovaf)
18480 * @since 2.0.0
18481 */
18482 if ( $this->_is_network_active &&
18483 ( fs_is_network_admin() || ! $this->is_delegated_connection() )
18484 ) {
18485 return is_super_admin();
18486 }
18487
18488 return ( $this->is_plugin() && current_user_can( is_multisite() ? 'manage_options' : 'activate_plugins' ) )
18489 || ( $this->is_theme() && current_user_can( 'switch_themes' ) );
18490 }
18491
18492 /**
18493 * Sync site's plan.
18494 *
18495 * @author Vova Feldman (@svovaf)
18496 * @since 1.0.3
18497 *
18498 * @uses FS_Api
18499 *
18500 * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by
18501 * the admin.
18502 * @param bool $is_context_single_site @since 2.0.0. This is used when syncing a license for a single install from the
18503 * network-level "Account" page.
18504 * @param int|null $current_blog_id @since 2.2.3. This is passed from the `execute_cron` method and used by the
18505 * `_sync_plugin_license` method in order to switch to the previous blog when sending
18506 * updates for a single site in case `execute_cron` has switched to a different blog.
18507 */
18508 private function _sync_license( $background = false, $is_context_single_site = false, $current_blog_id = null ) {
18509 $this->_logger->entrance();
18510
18511 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
18512
18513 $is_addon_sync = ( ! $this->_plugin->is_addon() && $plugin_id != $this->get_id() );
18514
18515 if ( $is_addon_sync ) {
18516 $this->_sync_addon_license( $plugin_id, $background );
18517 } else {
18518 $this->_sync_plugin_license( $background, true, $is_context_single_site, $current_blog_id );
18519 }
18520
18521 $this->do_action( 'after_account_plan_sync', $this->get_plan_name() );
18522 }
18523
18524 /**
18525 * Sync plugin's add-on license.
18526 *
18527 * @author Vova Feldman (@svovaf)
18528 * @since 1.0.6
18529 * @uses FS_Api
18530 *
18531 * @param number $addon_id
18532 * @param bool $background
18533 */
18534 private function _sync_addon_license( $addon_id, $background ) {
18535 $this->_logger->entrance();
18536
18537 if ( $this->is_addon_activated( $addon_id ) ) {
18538 // If already installed, use add-on sync.
18539 $fs_addon = self::get_instance_by_id( $addon_id );
18540
18541 if (
18542 // Add-on is network activated and network integrated.
18543 $fs_addon->is_network_active() ||
18544 // Background sync cron.
18545 self::is_cron() ||
18546 // Add-on is not network activated or not network integrated.
18547 ! fs_is_network_admin()
18548 ) {
18549 $fs_addon->_sync_license( $background );
18550
18551 return;
18552 }
18553 }
18554
18555 // Validate add-on exists.
18556 $addon = $this->get_addon( $addon_id );
18557
18558 if ( ! is_object( $addon ) ) {
18559 return;
18560 }
18561
18562 // Add add-on into account add-ons.
18563 $account_addons = $this->get_account_addons();
18564 if ( ! is_array( $account_addons ) ) {
18565 $account_addons = array();
18566 }
18567 $account_addons[] = $addon->id;
18568 $account_addons = array_unique( $account_addons );
18569 $this->_store_account_addons( $account_addons );
18570
18571 // Load add-on licenses.
18572 $licenses = $this->_fetch_licenses( $addon->id );
18573
18574 // Sync add-on licenses.
18575 if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) {
18576 $this->_update_licenses( $licenses, $addon->id );
18577
18578 if ( ! $this->is_addon_installed( $addon->id ) && FS_License_Manager::has_premium_license( $licenses ) ) {
18579 $plans_result = $this->get_api_site_or_plugin_scope()->get( $this->add_show_pending( "/addons/{$addon_id}/plans.json" ) );
18580
18581 if ( ! isset( $plans_result->error ) ) {
18582 $plans = array();
18583 foreach ( $plans_result->plans as $plan ) {
18584 $plans[] = new FS_Plugin_Plan( $plan );
18585 }
18586
18587 $this->_admin_notices->add_sticky(
18588 sprintf(
18589 ( FS_Plan_Manager::instance()->has_free_plan( $plans ) ?
18590 $this->get_text_inline( 'Your %s Add-on plan was successfully upgraded.', 'addon-successfully-upgraded-message' ) :
18591 /* translators: %s:product name, e.g. Facebook add-on was successfully... */
18592 $this->get_text_inline( '%s Add-on was successfully purchased.', 'addon-successfully-purchased-message' ) ),
18593 $addon->title
18594 ) . ' ' . $this->get_latest_download_link(
18595 $this->get_text_inline( 'Download the latest version', 'download-latest-version' ),
18596 $addon_id
18597 ),
18598 'addon_plan_upgraded_' . $addon->slug,
18599 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
18600 );
18601 }
18602 }
18603 }
18604 }
18605
18606 /**
18607 * Sync site's plugin plan.
18608 *
18609 * @author Vova Feldman (@svovaf)
18610 * @since 1.0.6
18611 * @uses FS_Api
18612 *
18613 * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by the admin.
18614 * @param bool $send_installs_update Since 2.0.0
18615 * @param bool $is_context_single_site Since 2.0.0. This is used when sending an update for a single install and
18616 * syncing its license from the network-level "Account" page (e.g.: after
18617 * activating a license only for the single install).
18618 * @param int|null $current_blog_id Since 2.2.3. This is passed from the `execute_cron` method so that it
18619 * can be used here to switch to the previous blog in case `execute_cron`
18620 * has switched to a different blog.
18621 */
18622 private function _sync_plugin_license(
18623 $background = false,
18624 $send_installs_update = true,
18625 $is_context_single_site = false,
18626 $current_blog_id = null
18627 ) {
18628 $this->_logger->entrance();
18629
18630 $plan_change = 'none';
18631
18632 $is_site_level_sync = ( $is_context_single_site || fs_is_blog_admin() || ! $this->_is_network_active );
18633
18634 if ( ! $send_installs_update ) {
18635 $site = $this->_site;
18636 } else {
18637 /**
18638 * Sync site info.
18639 *
18640 * @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.
18641 */
18642 if ( $is_site_level_sync ) {
18643 /**
18644 * Switch to the previous blog since `execute_cron` may have switched to a different blog.
18645 *
18646 * @author Leo Fajardo (@leorw)
18647 * @since 2.2.3
18648 */
18649 if ( is_numeric( $current_blog_id ) ) {
18650 $this->switch_to_blog( $current_blog_id );
18651 }
18652
18653 $result = $this->send_install_update( array(), true );
18654 $is_valid = $this->is_api_result_entity( $result );
18655 } else {
18656 $result = $this->send_installs_update( array(), true );
18657 $is_valid = $this->is_api_result_object( $result, 'installs' );
18658 }
18659
18660 if ( ! $is_valid ) {
18661 if ( $is_context_single_site ) {
18662 // Switch back to the main blog so that the following logic will have the right entities.
18663 $this->switch_to_blog( $this->_storage->network_install_blog_id );
18664 }
18665
18666 // Show API messages only if not background sync or if paying customer.
18667 if ( ! $background || $this->is_paying() ) {
18668 // Try to ping API to see if not blocked.
18669 if ( ! FS_Api::test() ) {
18670 /**
18671 * Failed to ping API - blocked!
18672 *
18673 * @author Vova Feldman (@svovaf)
18674 * @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.
18675 */
18676 $api = $this->get_api_site_scope();
18677
18678 if ( ! self::$_global_admin_notices->has_sticky( 'api_blocked' ) ) {
18679 self::$_global_admin_notices->add(
18680 sprintf(
18681 $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' ),
18682 $this->get_plugin_name(),
18683 '<b>' . implode( ', ', $this->apply_filters( 'api_domains', array(
18684 'api.freemius.com',
18685 'wp.freemius.com'
18686 ) ) ) . '</b>'
18687 ) . '<br> ' . $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . var_export( $result->error, true ),
18688 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
18689 'error',
18690 $background,
18691 'api_blocked'
18692 );
18693 }
18694 } else {
18695 // Authentication params are broken.
18696 $this->_admin_notices->add(
18697 $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' ),
18698 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
18699 'error'
18700 );
18701 }
18702 }
18703
18704 // No reason to continue with license sync while there are API issues.
18705 return;
18706 }
18707
18708 if ( $is_site_level_sync ) {
18709 $site = new FS_Site( $result );
18710 } else {
18711 // Map site addresses to their blog IDs.
18712 $address_to_blog_map = $this->get_address_to_blog_map();
18713
18714 // Find the current context install.
18715 $site = null;
18716 foreach ( $result->installs as $install ) {
18717 if ( $install->id == $this->_site->id ) {
18718 $site = new FS_Site( $install );
18719 } else {
18720 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
18721 $blog_id = $address_to_blog_map[ $address ];
18722
18723 $this->_store_site( true, $blog_id, new FS_Site( $install ) );
18724 }
18725 }
18726 }
18727
18728 // Sync plans.
18729 $this->_sync_plans();
18730 }
18731
18732 // Remove sticky API connectivity message.
18733 self::$_global_admin_notices->remove_sticky( 'api_blocked' );
18734
18735 if ( ! $this->has_paid_plan() ) {
18736 $this->_site = $site;
18737 $this->_store_site(
18738 true,
18739 $is_site_level_sync ?
18740 null :
18741 $this->get_network_install_blog_id()
18742 );
18743 } else {
18744 $context_blog_id = 0;
18745
18746 if ( $is_context_single_site ) {
18747 $context_blog_id = get_current_blog_id();
18748
18749 // Switch back to the main blog in order to properly sync the license.
18750 $this->switch_to_blog( $this->_storage->network_install_blog_id );
18751 }
18752
18753 /**
18754 * Sync licenses. Pass the site's license ID so that the foreign licenses will be fetched if the license
18755 * associated with that ID is not included in the user's licenses collection.
18756 */
18757 $this->_sync_licenses(
18758 $site->license_id,
18759 ( $is_context_single_site ?
18760 $context_blog_id :
18761 null
18762 )
18763 );
18764
18765 if ( $is_context_single_site ) {
18766 $this->switch_to_blog( $context_blog_id );
18767 }
18768
18769 // Check if plan / license changed.
18770 if ( $site->plan_id != $this->_site->plan_id ||
18771 // Check if trial started.
18772 $site->trial_plan_id != $this->_site->trial_plan_id ||
18773 $site->trial_ends != $this->_site->trial_ends ||
18774 // Check if license changed.
18775 $site->license_id != $this->_site->license_id
18776 ) {
18777 if ( $site->is_trial() && ( ! $this->_site->is_trial() || $site->trial_ends != $this->_site->trial_ends ) ) {
18778 // New trial started.
18779 $this->_site = $site;
18780 $plan_change = 'trial_started';
18781
18782 // For trial with subscription use-case.
18783 $new_license = is_null( $site->license_id ) ? null : $this->_get_license_by_id( $site->license_id );
18784
18785 if ( is_object( $new_license ) && $new_license->is_valid() ) {
18786 $this->_site = $site;
18787 $this->_update_site_license( $new_license );
18788 $this->_store_licenses();
18789
18790 $this->_sync_site_subscription( $this->_license );
18791 }
18792 } else if ( $this->_site->is_trial() && ! $site->is_trial() && ! is_numeric( $site->license_id ) ) {
18793 // Was in trial, but now trial expired and no license ID.
18794 // New trial started.
18795 $this->_site = $site;
18796 $plan_change = 'trial_expired';
18797 } else {
18798 $is_free = $this->is_free_plan();
18799
18800 // Make sure license exist and not expired.
18801 $new_license = is_null( $site->license_id ) ?
18802 null :
18803 $this->_get_license_by_id( $site->license_id );
18804
18805 if ( $is_free && is_null( $new_license ) && $this->has_any_license() && $this->_license->is_cancelled ) {
18806 // License cancelled.
18807 $this->_site = $site;
18808 $this->_update_site_license( $new_license );
18809 $this->_store_licenses();
18810
18811 $plan_change = 'cancelled';
18812 } else if ( $is_free && ( ( ! is_object( $new_license ) || $new_license->is_expired() ) ) ) {
18813 // The license is expired, so ignore upgrade method.
18814 $this->_site = $site;
18815 } else {
18816 // License changed.
18817 $this->_site = $site;
18818
18819 /**
18820 * IMPORTANT:
18821 * 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.
18822 *
18823 * @author Vova Feldman (@svovaf)
18824 * @since 2.0.0
18825 */
18826 $this->_update_site_license( $new_license );
18827
18828 if ( ! $is_context_single_site &&
18829 fs_is_network_admin() &&
18830 $this->_is_network_active &&
18831 $new_license->quota > 1 &&
18832 get_blog_count() > 1
18833 ) {
18834 // See if license can activated on all sites.
18835 if ( ! $this->try_activate_license_on_network( $this->_user, $new_license ) ) {
18836 if ( ! fs_request_get_bool( 'auto_install' ) ) {
18837 // Open the license activation dialog box on the account page.
18838 add_action( 'admin_footer', array(
18839 &$this,
18840 '_open_license_activation_dialog_box'
18841 ) );
18842 }
18843 }
18844 }
18845
18846 $this->_store_licenses();
18847
18848 $plan_change = $is_free ?
18849 ( $this->is_only_premium() ? 'activated' : 'upgraded' ) :
18850 ( is_object( $new_license ) ?
18851 'changed' :
18852 'downgraded' );
18853 }
18854 }
18855
18856 // Store updated site info.
18857 $this->_store_site(
18858 true,
18859 $is_site_level_sync ?
18860 null :
18861 $this->get_network_install_blog_id()
18862 );
18863 } else {
18864 if ( is_object( $this->_license ) && $this->_license->is_expired() ) {
18865 if ( ! $this->has_features_enabled_license() ) {
18866 $this->_deactivate_license();
18867 $plan_change = 'downgraded';
18868 } else {
18869 $plan_change = 'expired';
18870 }
18871 }
18872
18873 if ( is_numeric( $site->license_id ) && is_object( $this->_license ) ) {
18874 $this->_sync_site_subscription( $this->_license );
18875 }
18876 }
18877
18878 if ( $this->is_addon() || $this->has_addons() ) {
18879 /**
18880 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
18881 * an updated valid user licenses collection will be fetched from the server which is used to also
18882 * update the account add-ons (add-ons the user has licenses for).
18883 *
18884 * @author Leo Fajardo (@leorw)
18885 * @since 2.2.4
18886 */
18887 $this->purge_valid_user_licenses_cache();
18888 }
18889 }
18890
18891 $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...';
18892
18893 if ( $this->has_paid_plan() ) {
18894 switch ( $plan_change ) {
18895 case 'none':
18896 if ( ! $background && is_admin() ) {
18897 $plan = $this->is_trial() ?
18898 $this->get_trial_plan() :
18899 $this->get_plan();
18900
18901 if ( $plan->is_free() ) {
18902 $this->_admin_notices->add(
18903 sprintf(
18904 $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' ),
18905 '<i><b>' . $plan->title . ( $this->is_trial() ? ' ' . $this->get_text_x_inline( 'Trial', 'trial period', 'trial' ) : '' ) . '</b></i>'
18906 ) . ' ' . sprintf(
18907 '<a href="%s">%s</a>',
18908 $this->contact_url(
18909 'bug',
18910 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' ),
18911 strtoupper( $plan->name )
18912 )
18913 ),
18914 $this->get_text_inline( 'Please contact us here', 'contact-us-here' )
18915 ),
18916 $hmm_text
18917 );
18918 }
18919 }
18920 break;
18921 case 'upgraded':
18922 case 'activated':
18923 $this->_admin_notices->add_sticky(
18924 ( 'activated' === $plan_change ) ?
18925 $this->get_text_inline( 'Your plan was successfully activated.', 'plan-activated-message' ) :
18926 $this->get_text_inline( 'Your plan was successfully upgraded.', 'plan-upgraded-message' ) .
18927 $this->get_complete_upgrade_instructions(),
18928 'plan_upgraded',
18929 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
18930 );
18931
18932 $this->_admin_notices->remove_sticky( array(
18933 'trial_started',
18934 'trial_promotion',
18935 'trial_expired',
18936 'activation_complete',
18937 'license_expired',
18938 ) );
18939 break;
18940 case 'changed':
18941 $this->_admin_notices->add_sticky(
18942 sprintf(
18943 $this->get_text_inline( 'Your plan was successfully changed to %s.', 'plan-changed-to-x-message' ),
18944 $this->get_plan_title()
18945 ),
18946 'plan_changed'
18947 );
18948
18949 $this->_admin_notices->remove_sticky( array(
18950 'trial_started',
18951 'trial_promotion',
18952 'trial_expired',
18953 'activation_complete',
18954 ) );
18955 break;
18956 case 'downgraded':
18957 $this->_admin_notices->add_sticky(
18958 ($this->has_free_plan() ?
18959 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 ) :
18960 /* translators: %1$s: product title; %2$s, %3$s: wrapping HTML anchor element; %4$s: 'plugin', 'theme', or 'add-on'. */
18961 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) ) ),
18962 'license_expired',
18963 $hmm_text
18964 );
18965 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
18966 break;
18967 case 'cancelled':
18968 $this->_admin_notices->add(
18969 $this->get_text_inline( 'Your license has been cancelled. If you think it\'s a mistake, please contact support.', 'license-cancelled' ) . ' ' .
18970 sprintf(
18971 '<a href="%s">%s</a>',
18972 $this->contact_url( 'bug' ),
18973 $this->get_text_inline( 'Please contact us here', 'contact-us-here' )
18974 ),
18975 $hmm_text,
18976 'error'
18977 );
18978 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
18979 break;
18980 case 'expired':
18981 $this->_admin_notices->add_sticky(
18982 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 ),
18983 'license_expired',
18984 $hmm_text
18985 );
18986 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
18987 break;
18988 case 'trial_started':
18989 $this->_admin_notices->add_sticky(
18990 sprintf(
18991 $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ),
18992 '<i>' . $this->get_plugin_name() . '</i>'
18993 ) . $this->get_complete_upgrade_instructions( $this->get_trial_plan()->title ),
18994 'trial_started',
18995 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
18996 );
18997
18998 $this->_admin_notices->remove_sticky( array(
18999 'trial_promotion',
19000 ) );
19001 break;
19002 case 'trial_expired':
19003 $this->_admin_notices->add_sticky(
19004 ($this->has_free_plan() ?
19005 $this->get_text_inline( 'Your free trial has expired. You can still continue using all our free features.', 'trial-expired-message' ) :
19006 /* translators: %1$s: product title; %2$s, %3$s: wrapping HTML anchor element; %4$s: 'plugin', 'theme', or 'add-on'. */
19007 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))),
19008 'trial_expired',
19009 $hmm_text
19010 );
19011 $this->_admin_notices->remove_sticky( array(
19012 'trial_started',
19013 'trial_promotion',
19014 'plan_upgraded',
19015 ) );
19016 break;
19017 }
19018 }
19019
19020 if ( 'none' !== $plan_change ) {
19021 $this->do_action( 'after_license_change', $plan_change, $this->get_plan() );
19022 }
19023 }
19024
19025 /**
19026 * Include the required JS at the footer of the admin to trigger the license activation dialog box.
19027 *
19028 * @author Vova Feldman (@svovaf)
19029 * @since 2.0.0
19030 */
19031 public function _open_license_activation_dialog_box() {
19032 $vars = array( 'license_id' => $this->_site->license_id );
19033 fs_require_once_template( 'js/open-license-activation.php', $vars );
19034 }
19035
19036 /**
19037 * @author Vova Feldman (@svovaf)
19038 * @since 1.0.5
19039 *
19040 * @param bool $background
19041 */
19042 protected function _activate_license( $background = false, $premium_license = null ) {
19043 $this->_logger->entrance();
19044
19045 if ( is_null( $premium_license ) ) {
19046 $license_id = fs_request_get( 'license_id' );
19047
19048 if ( is_object( $this->_site ) &&
19049 FS_Plugin_License::is_valid_id( $license_id ) &&
19050 $license_id == $this->_site->license_id
19051 ) {
19052 // License is already activated.
19053 return;
19054 }
19055
19056 $premium_license = FS_Plugin_License::is_valid_id( $license_id ) ?
19057 $this->_get_license_by_id( $license_id ) :
19058 $this->_get_available_premium_license();
19059 }
19060
19061 if ( ! is_object( $premium_license ) ) {
19062 return;
19063 }
19064
19065 if ( ! is_object( $this->_site ) ) {
19066 // Not yet opted-in.
19067 $user = $this->get_current_or_network_user();
19068 if ( ! is_object( $user ) ) {
19069 $user = self::_get_user_by_id( $premium_license->user_id );
19070 }
19071
19072 if ( is_object( $user ) ) {
19073 $this->install_with_user( $user, $premium_license->secret_key, false, false, false );
19074 } else {
19075 $this->opt_in(
19076 false,
19077 false,
19078 false,
19079 $premium_license->secret_key
19080 );
19081
19082 return;
19083 }
19084 }
19085
19086
19087 /**
19088 * If the premium license is already associated with the install, just
19089 * update the license reference (activation is not required).
19090 *
19091 * @since 1.1.9
19092 */
19093 if ( $premium_license->id == $this->_site->license_id ) {
19094 // License is already activated.
19095 $this->_update_site_license( $premium_license );
19096 $this->_store_account();
19097
19098 return;
19099 }
19100
19101 if ( $this->_site->user_id != $premium_license->user_id ) {
19102 $api_request_params = array( 'license_key' => $premium_license->secret_key );
19103 } else {
19104 $api_request_params = array();
19105 }
19106
19107 $api = $this->get_api_site_scope();
19108 $license = $api->call( "/licenses/{$premium_license->id}.json?is_enriched=true", 'put', $api_request_params );
19109
19110 if ( ! $this->is_api_result_entity( $license ) ) {
19111 if ( ! $background ) {
19112 $this->_admin_notices->add( sprintf(
19113 '%s %s',
19114 $this->get_text_inline( 'It looks like the license could not be activated.', 'license-activation-failed-message' ),
19115 ( is_object( $license ) && isset( $license->error ) ?
19116 $license->error->message :
19117 sprintf( '%s<br><code>%s</code>',
19118 $this->get_text_inline( 'Error received from the server:', 'server-error-message' ),
19119 var_export( $license, true )
19120 )
19121 )
19122 ),
19123 $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...',
19124 'error'
19125 );
19126 }
19127
19128 return;
19129 }
19130
19131 $premium_license = new FS_Plugin_License( $license );
19132
19133 // Updated site plan.
19134 $site = $this->get_api_site_scope()->get( '/', true );
19135 if ( $this->is_api_result_entity( $site ) ) {
19136 $this->_site = new FS_Site( $site );
19137 }
19138 $this->_update_site_license( $premium_license );
19139
19140 $this->_store_account();
19141
19142 if ( $this->is_addon() || $this->has_addons() ) {
19143 /**
19144 * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
19145 * an updated valid user licenses collection will be fetched from the server which is used to also
19146 * update the account add-ons (add-ons the user has licenses for).
19147 *
19148 * @author Leo Fajardo (@leorw)
19149 * @since 2.2.4
19150 */
19151 $this->purge_valid_user_licenses_cache();
19152 }
19153
19154 if ( ! $background ) {
19155 $this->_admin_notices->add_sticky(
19156 $this->get_text_inline( 'Your license was successfully activated.', 'license-activated-message' ) .
19157 $this->get_complete_upgrade_instructions(),
19158 'license_activated',
19159 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
19160 );
19161 }
19162
19163 $this->_admin_notices->remove_sticky( array(
19164 'trial_promotion',
19165 'license_expired',
19166 ) );
19167 }
19168
19169 /**
19170 * @author Vova Feldman (@svovaf)
19171 * @since 1.0.5
19172 *
19173 * @param bool $show_notice
19174 */
19175 protected function _deactivate_license( $show_notice = true ) {
19176 $this->_logger->entrance();
19177
19178 $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...';
19179
19180 if ( ! FS_Plugin_License::is_valid_id( $this->_site->license_id ) ) {
19181 $this->_admin_notices->add(
19182 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() ),
19183 $hmm_text
19184 );
19185
19186 return;
19187 }
19188
19189 $api = $this->get_api_site_scope();
19190 $license = $api->call( "/licenses/{$this->_site->license_id}.json", 'delete' );
19191
19192 $this->handle_license_deactivation_result( $license, $hmm_text, $show_notice );
19193 }
19194
19195 /**
19196 * @author Leo Fajardo (@leorw)
19197 * @since 2.2.1
19198 *
19199 * @param FS_Plugin_License $license
19200 * @param bool|string $hmm_text
19201 * @param bool $show_notice
19202 */
19203 private function handle_license_deactivation_result( $license, $hmm_text = false, $show_notice = true ) {
19204 if ( isset( $license->error ) ) {
19205 $this->_admin_notices->add(
19206 $this->get_text_inline( 'It looks like the license deactivation failed.', 'license-deactivation-failed-message' ) . '<br> ' .
19207 $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . ' ' . var_export( $license->error, true ),
19208 $hmm_text,
19209 'error'
19210 );
19211
19212 return;
19213 }
19214
19215 // Update license cache.
19216 if ( is_array( $this->_licenses ) ) {
19217 for ( $i = 0, $len = count( $this->_licenses ); $i < $len; $i ++ ) {
19218 if ( $license->id == $this->_licenses[ $i ]->id ) {
19219 $this->_licenses[ $i ] = new FS_Plugin_License( $license );
19220 }
19221 }
19222 }
19223
19224 // Update site plan to default.
19225 $this->_sync_plans();
19226 $this->_site->plan_id = $this->_plans[0]->id;
19227 // Unlink license from site.
19228 $this->_update_site_license( null );
19229
19230 $this->_store_account();
19231
19232 if ( $show_notice ) {
19233 $this->_admin_notices->add(
19234 sprintf( $this->get_text_inline( 'Your license was successfully deactivated, you are back to the %s plan.', 'license-deactivation-message' ), $this->get_plan_title() ),
19235 $this->get_text_inline( 'O.K', 'ok' )
19236 );
19237 }
19238
19239 $this->_admin_notices->remove_sticky( array(
19240 'plan_upgraded',
19241 'license_activated',
19242 ) );
19243 }
19244
19245 /**
19246 * Site plan downgrade.
19247 *
19248 * @author Vova Feldman (@svovaf)
19249 * @since 1.0.4
19250 *
19251 * @return object
19252 *
19253 * @uses FS_Api
19254 */
19255 private function _downgrade_site() {
19256 $this->_logger->entrance();
19257
19258 $deactivate_license = fs_request_get_bool( 'deactivate_license' );
19259
19260 $api = $this->get_api_site_scope();
19261 $site = $api->call( 'downgrade.json', 'put', array( 'deactivate_license' => $deactivate_license ) );
19262
19263 $plan_downgraded = false;
19264 $plan = false;
19265 if ( $this->is_api_result_entity( $site ) ) {
19266 $prev_plan_id = $this->_site->plan_id;
19267
19268 // Update new site plan id.
19269 $this->_site->plan_id = $site->plan_id;
19270
19271 $plan = $this->get_plan();
19272 $subscription = $this->_sync_site_subscription( $this->_license );
19273
19274 // Plan downgraded if plan was changed or subscription was cancelled.
19275 $plan_downgraded = ( $plan instanceof FS_Plugin_Plan && $prev_plan_id != $plan->id ) ||
19276 ( is_object( $subscription ) && ! isset( $subscription->error ) && ! $subscription->is_active() );
19277 } else {
19278 // handle different error cases.
19279 $this->handle_license_deactivation_result(
19280 $site,
19281 $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...'
19282 );
19283 }
19284
19285 if ( ! $plan_downgraded ) {
19286 return (object) array(
19287 'error' => (object) array(
19288 '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' )
19289 )
19290 );
19291 }
19292
19293 // Remove previous sticky message about upgrade (if exist).
19294 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
19295
19296 $this->_admin_notices->add(
19297 sprintf( $this->get_text_inline( 'Your subscription was successfully cancelled. Your %s plan license will expire in %s.', 'plan-x-downgraded-message' ),
19298 $plan->title,
19299 human_time_diff( time(), strtotime( $this->_license->expiration ) )
19300 )
19301 );
19302
19303 // Store site updates.
19304 $this->_store_site();
19305
19306 if ( $deactivate_license &&
19307 ! FS_Plugin_License::is_valid_id( $site->license_id )
19308 ) {
19309 if ( $this->_site->is_localhost() ) {
19310 $this->_license->activated_local = max( 0, $this->_license->activated_local - 1 );
19311 } else {
19312 $this->_license->activated = max( 0, $this->_license->activated - 1 );
19313 }
19314
19315 // Handle successful license deactivation result.
19316 $this->handle_license_deactivation_result( $this->_license );
19317 }
19318
19319 return $site;
19320 }
19321
19322 /**
19323 * @author Vova Feldman (@svovaf)
19324 * @since 1.1.8.1
19325 *
19326 * @param bool|string $plan_name
19327 *
19328 * @return bool If trial was successfully started.
19329 */
19330 function start_trial( $plan_name = false ) {
19331 $this->_logger->entrance();
19332
19333 // Alias.
19334 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
19335
19336 if ( $this->is_trial() ) {
19337 // Already in trial mode.
19338 $this->_admin_notices->add(
19339 sprintf( $this->get_text_inline( 'You are already running the %s in a trial mode.', 'in-trial-mode' ), $this->_module_type ),
19340 $oops_text,
19341 'error'
19342 );
19343
19344 return false;
19345 }
19346
19347 if ( $this->_site->is_trial_utilized() ) {
19348 // Trial was already utilized.
19349 $this->_admin_notices->add(
19350 $this->get_text_inline( 'You already utilized a trial before.', 'trial-utilized' ),
19351 $oops_text,
19352 'error'
19353 );
19354
19355 return false;
19356 }
19357
19358 if ( false !== $plan_name ) {
19359 $plan = $this->get_plan_by_name( $plan_name );
19360
19361 if ( false === $plan ) {
19362 // Plan doesn't exist.
19363 $this->_admin_notices->add(
19364 sprintf( $this->get_text_inline( 'Plan %s do not exist, therefore, can\'t start a trial.', 'trial-plan-x-not-exist' ), $plan_name ),
19365 $oops_text,
19366 'error'
19367 );
19368
19369 return false;
19370 }
19371
19372 if ( ! $plan->has_trial() ) {
19373 // Plan doesn't exist.
19374 $this->_admin_notices->add(
19375 sprintf( $this->get_text_inline( 'Plan %s does not support a trial period.', 'plan-x-no-trial' ), $plan_name ),
19376 $oops_text,
19377 'error'
19378 );
19379
19380 return false;
19381 }
19382 } else {
19383 if ( ! $this->has_trial_plan() ) {
19384 // None of the plans have a trial.
19385 $this->_admin_notices->add(
19386 sprintf( $this->get_text_inline( 'None of the %s\'s plans supports a trial period.', 'no-trials' ), $this->_module_type ),
19387 $oops_text,
19388 'error'
19389 );
19390
19391 return false;
19392 }
19393
19394 $plans_with_trial = FS_Plan_Manager::instance()->get_trial_plans( $this->_plans );
19395
19396 $plan = $plans_with_trial[0];
19397 }
19398
19399 $api = $this->get_api_site_scope();
19400 $plan = $api->call( "plans/{$plan->id}/trials.json", 'post' );
19401
19402 if ( ! $this->is_api_result_entity( $plan ) ) {
19403 // Some API error while trying to start the trial.
19404 $this->_admin_notices->add(
19405 sprintf( $this->get_text_inline( 'Unexpected API error. Please contact the %s\'s author with the following error.', 'unexpected-api-error' ), $this->_module_type )
19406 . ' ' . var_export( $plan, true ),
19407 $oops_text,
19408 'error'
19409 );
19410
19411 return false;
19412 }
19413
19414 // Sync license.
19415 $this->_sync_license();
19416
19417 return $this->is_trial();
19418 }
19419
19420 /**
19421 * Cancel site trial.
19422 *
19423 * @author Vova Feldman (@svovaf)
19424 * @since 1.0.9
19425 *
19426 * @return object
19427 *
19428 * @uses FS_Api
19429 */
19430 private function _cancel_trial() {
19431 $this->_logger->entrance();
19432
19433 if ( ! $this->is_trial() ) {
19434 return (object) array(
19435 'error' => (object) array(
19436 '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' )
19437 )
19438 );
19439 }
19440
19441 $trial_plan = $this->get_trial_plan();
19442
19443 $api = $this->get_api_site_scope();
19444 $site = $api->call( 'trials.json', 'delete' );
19445
19446 $trial_cancelled = false;
19447
19448 if ( $this->is_api_result_entity( $site ) ) {
19449 $prev_trial_ends = $this->_site->trial_ends;
19450
19451 if ( $this->is_paid_trial() ) {
19452 $this->_license->expiration = $site->trial_ends;
19453 $this->_license->is_cancelled = true;
19454 $this->_update_site_license( $this->_license );
19455 $this->_store_licenses();
19456
19457 // Clear subscription reference.
19458 $this->_sync_site_subscription( null );
19459 }
19460
19461 // Update site info.
19462 $this->_site = new FS_Site( $site );
19463
19464 $trial_cancelled = ( $prev_trial_ends != $site->trial_ends );
19465 } else {
19466 // @todo handle different error cases.
19467 }
19468
19469 if ( ! $trial_cancelled ) {
19470 return (object) array(
19471 'error' => (object) array(
19472 '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' )
19473 )
19474 );
19475 }
19476
19477 // Remove previous sticky messages about upgrade or trial (if exist).
19478 $this->_admin_notices->remove_sticky( array(
19479 'trial_started',
19480 'trial_promotion',
19481 'plan_upgraded',
19482 ) );
19483
19484 // Store site updates.
19485 $this->_store_site();
19486
19487 if ( ! $this->is_addon() ||
19488 ! $this->deactivate_premium_only_addon_without_license( true )
19489 ) {
19490 $this->_admin_notices->add(
19491 sprintf( $this->get_text_inline( 'Your %s free trial was successfully cancelled.', 'trial-cancel-message' ), $trial_plan->title )
19492 );
19493 }
19494
19495 return $site;
19496 }
19497
19498 /**
19499 * @author Vova Feldman (@svovaf)
19500 * @since 1.0.6
19501 *
19502 * @param bool|number $plugin_id
19503 *
19504 * @return bool
19505 */
19506 private function _is_addon_id( $plugin_id ) {
19507 return is_numeric( $plugin_id ) && ( $this->get_id() != $plugin_id );
19508 }
19509
19510 /**
19511 * Check if user eligible to download premium version updates.
19512 *
19513 * @author Vova Feldman (@svovaf)
19514 * @since 1.0.6
19515 *
19516 * @return bool
19517 */
19518 private function _can_download_premium() {
19519 return $this->has_any_active_valid_license() ||
19520 ( $this->is_trial() && ! $this->get_trial_plan()->is_free() );
19521 }
19522
19523 /**
19524 *
19525 * @author Vova Feldman (@svovaf)
19526 * @since 1.0.6
19527 *
19528 * @param bool|number $addon_id
19529 * @param string $type "json" or "zip"
19530 *
19531 * @return string
19532 */
19533 private function _get_latest_version_endpoint( $addon_id = false, $type = 'json' ) {
19534
19535 $is_addon = $this->_is_addon_id( $addon_id );
19536
19537 $is_premium = null;
19538 if ( ! $is_addon ) {
19539 $is_premium = ( $this->is_premium() || $this->_can_download_premium() );
19540 } else if ( $this->is_addon_activated( $addon_id ) ) {
19541 $fs_addon = self::get_instance_by_id( $addon_id );
19542 $is_premium = ( $fs_addon->is_premium() || $fs_addon->_can_download_premium() );
19543 }
19544
19545 // If add-on, then append add-on ID.
19546 $endpoint = ( $is_addon ? "/addons/$addon_id" : '' ) .
19547 '/updates/latest.' . $type;
19548
19549 // If add-on and not yet activated, try to fetch based on server licensing.
19550 if ( is_bool( $is_premium ) ) {
19551 $endpoint = add_query_arg( 'is_premium', json_encode( $is_premium ), $endpoint );
19552 }
19553
19554 if ( $this->has_secret_key() ) {
19555 $endpoint = add_query_arg( 'type', 'all', $endpoint );
19556 } else if ( $this->is_registered() && $this->_user->is_beta() ) {
19557 $endpoint = add_query_arg( 'type', 'beta', $endpoint );
19558 }
19559
19560 return $endpoint;
19561 }
19562
19563 /**
19564 * @author Vova Feldman (@svovaf)
19565 * @since 1.0.4
19566 *
19567 * @param bool|number $addon_id
19568 * @param bool $flush Since 1.1.7.3
19569 * @param int $expiration Since 1.2.2.7
19570 * @param bool|string $newer_than Since 2.2.1
19571 * @param bool|string $fetch_readme Since 2.2.1
19572 *
19573 * @return object|false Plugin latest tag info.
19574 */
19575 function _fetch_latest_version(
19576 $addon_id = false,
19577 $flush = true,
19578 $expiration = WP_FS__TIME_24_HOURS_IN_SEC,
19579 $newer_than = false,
19580 $fetch_readme = true
19581 ) {
19582 $this->_logger->entrance();
19583
19584 $switch_to_blog_id = null;
19585
19586 /**
19587 * @since 1.1.7.3 Check for plugin updates from Freemius only if opted-in.
19588 * @since 1.1.7.4 Also check updates for add-ons.
19589 */
19590 if ( ! $this->is_registered() &&
19591 ! $this->_is_addon_id( $addon_id )
19592 ) {
19593 if ( ! is_multisite() ) {
19594 return false;
19595 }
19596
19597 $installs_map = $this->get_blog_install_map();
19598
19599 foreach ( $installs_map as $blog_id => $install ) {
19600 /**
19601 * @var FS_Site $install
19602 */
19603 if ( $install->is_trial() ) {
19604 $switch_to_blog_id = $blog_id;
19605 break;
19606 }
19607
19608 if ( FS_Plugin_License::is_valid_id( $install->license_id ) ) {
19609 $license = $this->get_license_by_id( $install->license_id );
19610
19611 if ( is_object( $license ) && $license->is_features_enabled() ) {
19612 $switch_to_blog_id = $blog_id;
19613 break;
19614 }
19615 }
19616 }
19617
19618 if ( is_null( $switch_to_blog_id ) ) {
19619 return false;
19620 }
19621 }
19622
19623 $current_blog_id = is_numeric( $switch_to_blog_id ) ?
19624 get_current_blog_id() :
19625 0;
19626
19627 if ( is_numeric( $switch_to_blog_id ) ) {
19628 $this->switch_to_blog( $switch_to_blog_id );
19629 }
19630
19631 $latest_version_endpoint = $this->_get_latest_version_endpoint( $addon_id, 'json' );
19632
19633 if ( ! empty( $newer_than ) ) {
19634 $latest_version_endpoint = add_query_arg( 'newer_than', $newer_than, $latest_version_endpoint );
19635 }
19636
19637 if ( true === $fetch_readme ) {
19638 $latest_version_endpoint = add_query_arg( 'readme', 'true', $latest_version_endpoint );
19639 }
19640
19641 $tag = $this->get_api_site_or_plugin_scope()->get(
19642 $latest_version_endpoint,
19643 $flush,
19644 $expiration
19645 );
19646
19647 if ( is_numeric( $switch_to_blog_id ) ) {
19648 $this->switch_to_blog( $current_blog_id );
19649 }
19650
19651 $latest_version = ( is_object( $tag ) && isset( $tag->version ) ) ? $tag->version : 'couldn\'t get';
19652
19653 $this->_logger->departure( 'Latest version ' . $latest_version );
19654
19655 return ( is_object( $tag ) && isset( $tag->version ) ) ? $tag : false;
19656 }
19657
19658 #----------------------------------------------------------------------------------
19659 #region Download Plugin
19660 #----------------------------------------------------------------------------------
19661
19662 /**
19663 * Download latest plugin version, based on plan.
19664 *
19665 * Not like _download_latest(), this will redirect the page
19666 * to secure download url to prevent dual download (from FS to WP server,
19667 * and then from WP server to the client / browser).
19668 *
19669 * @author Vova Feldman (@svovaf)
19670 * @since 1.0.9
19671 *
19672 * @param bool|number $plugin_id
19673 *
19674 * @uses FS_Api
19675 * @uses wp_redirect()
19676 */
19677 private function download_latest_directly( $plugin_id = false ) {
19678 $this->_logger->entrance();
19679
19680 wp_redirect( $this->get_latest_download_api_url( $plugin_id ) );
19681 }
19682
19683 /**
19684 * Get latest plugin FS API download URL.
19685 *
19686 * @author Vova Feldman (@svovaf)
19687 * @since 1.0.9
19688 *
19689 * @param bool|number $plugin_id
19690 *
19691 * @return string
19692 */
19693 private function get_latest_download_api_url( $plugin_id = false ) {
19694 $this->_logger->entrance();
19695
19696 return $this->get_api_site_scope()->get_signed_url(
19697 $this->_get_latest_version_endpoint( $plugin_id, 'zip' )
19698 );
19699 }
19700
19701 /**
19702 * Get payment invoice URL.
19703 *
19704 * @author Vova Feldman (@svovaf)
19705 * @since 1.2.0
19706 *
19707 * @param bool|number $payment_id
19708 *
19709 * @return string
19710 */
19711 function _get_invoice_api_url( $payment_id = false ) {
19712 $this->_logger->entrance();
19713
19714 $url = $this->get_api_user_scope()->get_signed_url(
19715 "/payments/{$payment_id}/invoice.pdf"
19716 );
19717
19718 if ( ! fs_starts_with( $url, 'https://' ) ) {
19719 // Always use HTTPS for invoices.
19720 $url = 'https' . substr( $url, 4 );
19721 }
19722
19723 return $url;
19724 }
19725
19726 /**
19727 * Get latest plugin download link.
19728 *
19729 * @author Vova Feldman (@svovaf)
19730 * @since 1.0.9
19731 *
19732 * @param string $label
19733 * @param bool|number $plugin_id
19734 *
19735 * @return string
19736 */
19737 private function get_latest_download_link( $label, $plugin_id = false ) {
19738 return sprintf(
19739 '<a target="_blank" href="%s">%s</a>',
19740 $this->_get_latest_download_local_url( $plugin_id ),
19741 $label
19742 );
19743 }
19744
19745 /**
19746 * Get latest plugin download local URL.
19747 *
19748 * @author Vova Feldman (@svovaf)
19749 * @since 1.0.9
19750 *
19751 * @param bool|number $plugin_id
19752 *
19753 * @return string
19754 */
19755 function _get_latest_download_local_url( $plugin_id = false ) {
19756 // Add timestamp to protect from caching.
19757 $params = array( 'ts' => WP_FS__SCRIPT_START_TIME );
19758
19759 if ( ! empty( $plugin_id ) ) {
19760 $params['plugin_id'] = $plugin_id;
19761 } else if ( $this->is_addon() ) {
19762 $params['plugin_id'] = $this->get_id();
19763 }
19764
19765 $fs = $this->is_addon() ?
19766 $this->get_parent_instance() :
19767 $this;
19768
19769 return $this->apply_filters( 'download_latest_url', $fs->get_account_url( 'download_latest', $params ) );
19770 }
19771
19772 #endregion Download Plugin ------------------------------------------------------------------
19773
19774 /**
19775 * @author Vova Feldman (@svovaf)
19776 * @since 1.0.4
19777 *
19778 * @uses FS_Api
19779 *
19780 * @param bool $background Hints the method if it's a background updates check. If false, it means that
19781 * was initiated by the admin.
19782 * @param bool|number $plugin_id
19783 * @param bool $flush Since 1.1.7.3
19784 * @param int $expiration Since 1.2.2.7
19785 * @param bool|string $newer_than Since 2.2.1
19786 */
19787 private function check_updates(
19788 $background = false,
19789 $plugin_id = false,
19790 $flush = true,
19791 $expiration = WP_FS__TIME_24_HOURS_IN_SEC,
19792 $newer_than = false
19793 ) {
19794 $this->_logger->entrance();
19795
19796 // Check if there's a newer version for download.
19797 $new_version = $this->_fetch_newer_version( $plugin_id, $flush, $expiration, $newer_than );
19798
19799 $update = null;
19800 if ( is_object( $new_version ) ) {
19801 $update = new FS_Plugin_Tag( $new_version );
19802
19803 if ( ! $background ) {
19804 $this->_admin_notices->add(
19805 sprintf(
19806 /* translators: %s: Numeric version number (e.g. '2.1.9' */
19807 $this->get_text_inline( 'Version %s was released.', 'version-x-released' ) . ' ' . $this->get_text_inline( 'Please download %s.', 'please-download-x' ),
19808 $update->version,
19809 sprintf(
19810 '<a href="%s" target="_blank">%s</a>',
19811 $this->get_account_url( 'download_latest' ),
19812 sprintf(
19813 /* translators: %s: plan name (e.g. latest "Professional" version) */
19814 $this->get_text_inline( 'the latest %s version here', 'latest-x-version' ),
19815 $this->get_plan_title()
19816 )
19817 )
19818 ),
19819 $this->get_text_inline( 'New', 'new' ) . '!'
19820 );
19821 }
19822 } else if ( false === $new_version && ! $background ) {
19823 $this->_admin_notices->add(
19824 $this->get_text_inline( 'Seems like you got the latest release.', 'you-have-latest' ),
19825 $this->get_text_inline( 'You are all good!', 'you-are-good' )
19826 );
19827 }
19828
19829 $this->_store_update( $update, true, $plugin_id );
19830 }
19831
19832 /**
19833 * @author Vova Feldman (@svovaf)
19834 * @since 1.0.4
19835 *
19836 * @param bool $flush Since 1.1.7.3 add 24 hour cache by default.
19837 *
19838 * @return FS_Plugin[]
19839 *
19840 * @uses FS_Api
19841 */
19842 private function sync_addons( $flush = false ) {
19843 $this->_logger->entrance();
19844
19845 $api = $this->get_api_site_or_plugin_scope();
19846
19847 $path = $this->add_show_pending( '/addons.json?enriched=true' );
19848
19849 /**
19850 * @since 1.2.1
19851 *
19852 * If there's a cached version of the add-ons and not asking
19853 * for a flush, just use the currently stored add-ons.
19854 */
19855 if ( ! $flush && $api->is_cached( $path ) ) {
19856 $addons = self::get_all_addons();
19857
19858 return isset( $addons[ $this->_plugin->id ] ) ?
19859 $addons[ $this->_plugin->id ] :
19860 array();
19861 }
19862
19863 $result = $api->get( $path, $flush );
19864
19865 $addons = array();
19866 if ( $this->is_api_result_object( $result, 'plugins' ) &&
19867 is_array( $result->plugins )
19868 ) {
19869 for ( $i = 0, $len = count( $result->plugins ); $i < $len; $i ++ ) {
19870 $addons[ $i ] = new FS_Plugin( $result->plugins[ $i ] );
19871 }
19872
19873 $this->_store_addons( $addons, true );
19874 }
19875
19876 return $addons;
19877 }
19878
19879 /**
19880 * Handle user email update.
19881 *
19882 * @author Vova Feldman (@svovaf)
19883 * @since 1.0.3
19884 * @uses FS_Api
19885 *
19886 * @param string $new_email
19887 *
19888 * @return object
19889 */
19890 private function update_email( $new_email ) {
19891 $this->_logger->entrance();
19892
19893
19894 $api = $this->get_api_user_scope();
19895 $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,email,is_verified", 'put', array(
19896 'email' => $new_email,
19897 'after_email_confirm_url' => $this->_get_admin_page_url(
19898 'account',
19899 array( 'fs_action' => 'sync_user' )
19900 ),
19901 ) );
19902
19903 if ( ! isset( $user->error ) ) {
19904 $this->_user->email = $user->email;
19905 $this->_user->is_verified = $user->is_verified;
19906 $this->_store_user();
19907 } else {
19908 // handle different error cases.
19909
19910 }
19911
19912 return $user;
19913 }
19914
19915 #----------------------------------------------------------------------------------
19916 #region API Error Handling
19917 #----------------------------------------------------------------------------------
19918
19919 /**
19920 * @author Vova Feldman (@svovaf)
19921 * @since 1.1.1
19922 *
19923 * @param mixed $result
19924 *
19925 * @return bool Is API result contains an error.
19926 */
19927 private function is_api_error( $result ) {
19928 return FS_Api::is_api_error( $result );
19929 }
19930
19931 /**
19932 * Checks if given API result is a non-empty and not an error object.
19933 *
19934 * @author Vova Feldman (@svovaf)
19935 * @since 1.2.1.5
19936 *
19937 * @param mixed $result
19938 * @param string|null $required_property Optional property we want to verify that is set.
19939 *
19940 * @return bool
19941 */
19942 function is_api_result_object( $result, $required_property = null ) {
19943 return FS_Api::is_api_result_object( $result, $required_property );
19944 }
19945
19946 /**
19947 * Checks if given API result is a non-empty entity object with non-empty ID.
19948 *
19949 * @author Vova Feldman (@svovaf)
19950 * @since 1.2.1.5
19951 *
19952 * @param mixed $result
19953 *
19954 * @return bool
19955 */
19956 private function is_api_result_entity( $result ) {
19957 return FS_Api::is_api_result_entity( $result );
19958 }
19959
19960 #endregion
19961
19962 /**
19963 * Make sure a given argument is an array of a specific type.
19964 *
19965 * @author Vova Feldman (@svovaf)
19966 * @since 1.2.1.5
19967 *
19968 * @param mixed $array
19969 * @param string $class
19970 *
19971 * @return bool
19972 */
19973 private function is_array_instanceof( $array, $class ) {
19974 return ( is_array( $array ) && ( empty( $array ) || $array[0] instanceof $class ) );
19975 }
19976
19977 /**
19978 * Start install ownership change.
19979 *
19980 * @author Vova Feldman (@svovaf)
19981 * @since 1.1.1
19982 * @uses FS_Api
19983 *
19984 * @param string $candidate_email
19985 *
19986 * @return bool Is ownership change successfully initiated.
19987 */
19988 private function init_change_owner( $candidate_email ) {
19989 $this->_logger->entrance();
19990
19991 $api = $this->get_api_site_scope();
19992 $result = $api->call( "/users/{$this->_user->id}.json", 'put', array(
19993 'email' => $candidate_email,
19994 'after_confirm_url' => $this->_get_admin_page_url(
19995 'account',
19996 array( 'fs_action' => 'change_owner' )
19997 ),
19998 ) );
19999
20000 return ! $this->is_api_error( $result );
20001 }
20002
20003 /**
20004 * Handle install ownership change.
20005 *
20006 * @author Vova Feldman (@svovaf)
20007 * @since 1.1.1
20008 * @uses FS_Api
20009 *
20010 * @return bool Was ownership change successfully complete.
20011 */
20012 private function complete_change_owner() {
20013 $this->_logger->entrance();
20014
20015 $site_result = $this->get_api_site_scope( true )->get();
20016 $site = new FS_Site( $site_result );
20017 $this->_site = $site;
20018
20019 $user = new FS_User();
20020 $user->id = fs_request_get( 'user_id' );
20021
20022 // Validate install's user and given user.
20023 if ( $user->id != $this->_site->user_id ) {
20024 return false;
20025 }
20026
20027 $user->public_key = fs_request_get( 'user_public_key' );
20028 $user->secret_key = fs_request_get( 'user_secret_key' );
20029
20030 // Fetch new user information.
20031 $this->_user = $user;
20032 $user_result = $this->get_api_user_scope( true )->get();
20033 $user = new FS_User( $user_result );
20034 $this->_user = $user;
20035
20036 $this->_set_account( $user, $site );
20037
20038 return true;
20039 }
20040
20041 /**
20042 * Handle user name update.
20043 *
20044 * @author Vova Feldman (@svovaf)
20045 * @since 1.0.9
20046 * @uses FS_Api
20047 *
20048 * @return object
20049 */
20050 private function update_user_name() {
20051 $this->_logger->entrance();
20052 $name = fs_request_get( 'fs_user_name_' . $this->get_unique_affix(), '' );
20053
20054 $api = $this->get_api_user_scope();
20055 $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,first,last", 'put', array(
20056 'name' => $name,
20057 ) );
20058
20059 if ( ! isset( $user->error ) ) {
20060 $this->_user->first = $user->first;
20061 $this->_user->last = $user->last;
20062 $this->_store_user();
20063 } else {
20064 // handle different error cases.
20065
20066 }
20067
20068 return $user;
20069 }
20070
20071 /**
20072 * Verify user email.
20073 *
20074 * @author Vova Feldman (@svovaf)
20075 * @since 1.0.3
20076 * @uses FS_Api
20077 */
20078 private function verify_email() {
20079 $this->_handle_account_user_sync();
20080
20081 if ( $this->_user->is_verified() ) {
20082 return;
20083 }
20084
20085 $api = $this->get_api_site_scope();
20086 $result = $api->call( "/users/{$this->_user->id}/verify.json", 'put', array(
20087 'after_email_confirm_url' => $this->_get_admin_page_url(
20088 'account',
20089 array( 'fs_action' => 'sync_user' )
20090 )
20091 ) );
20092
20093 if ( ! isset( $result->error ) ) {
20094 $this->_admin_notices->add( sprintf(
20095 $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' ),
20096 sprintf( '<a href="mailto:%1$s">%2$s</a>', esc_url( $this->_user->email ), $this->_user->email )
20097 ) );
20098 } else {
20099 // handle different error cases.
20100
20101 }
20102 }
20103
20104 /**
20105 * @author Vova Feldman (@svovaf)
20106 * @since 1.1.2
20107 *
20108 * @param array $params
20109 * @param bool|null $network
20110 *
20111 * @return string
20112 */
20113 function get_activation_url( $params = array(), $network = null ) {
20114 if ( $this->is_addon() && $this->has_free_plan() ) {
20115 /**
20116 * @author Vova Feldman (@svovaf)
20117 * @since 1.2.1.7 Add-on's activation is the parent's module activation.
20118 */
20119 return $this->get_parent_instance()->get_activation_url( $params );
20120 }
20121
20122 return $this->apply_filters( 'connect_url', $this->_get_admin_page_url( '', $params, $network ) );
20123 }
20124
20125 /**
20126 * @author Vova Feldman (@svovaf)
20127 * @since 1.2.1.5
20128 *
20129 * @param array $params
20130 *
20131 * @return string
20132 */
20133 function get_reconnect_url( $params = array() ) {
20134 $params['fs_action'] = 'reset_anonymous_mode';
20135 $params['fs_unique_affix'] = $this->get_unique_affix();
20136
20137 return $this->get_activation_url( $params );
20138 }
20139
20140 /**
20141 * Get the URL of the page that should be loaded after the user connect
20142 * or skip in the opt-in screen.
20143 *
20144 * @author Vova Feldman (@svovaf)
20145 * @since 1.1.3
20146 *
20147 * @param string $filter Filter name.
20148 * @param array $params Since 1.2.2.7
20149 * @param bool|null $network
20150 *
20151 * @return string
20152 */
20153 function get_after_activation_url( $filter, $params = array(), $network = null ) {
20154 if ( $this->is_free_wp_org_theme() &&
20155 ( fs_request_has( 'pending_activation' ) ||
20156 // For cases when the first time path is set, even though it's a WP.org theme.
20157 fs_request_get_bool( $this->get_unique_affix() . '_show_optin' ) )
20158 ) {
20159 $first_time_path = '';
20160 } else {
20161 $first_time_path = $this->_menu->get_first_time_path(
20162 fs_is_network_admin() && $this->_is_network_active
20163 );
20164 }
20165
20166 if ( $this->_is_network_active &&
20167 fs_is_network_admin() &&
20168 ! $this->_menu->has_network_menu() &&
20169 $this->is_network_registered()
20170 ) {
20171 $target_url = $this->get_account_url();
20172 } else {
20173 // Default plugin's page.
20174 $target_url = $this->_get_admin_page_url( '', array(), $network );
20175 }
20176
20177 return add_query_arg( $params, $this->apply_filters(
20178 $filter,
20179 empty( $first_time_path ) ?
20180 $target_url :
20181 $first_time_path
20182 ) );
20183 }
20184
20185 /**
20186 * Handle account page updates / edits / actions.
20187 *
20188 * @author Vova Feldman (@svovaf)
20189 * @since 1.0.2
20190 *
20191 */
20192 private function _handle_account_edits() {
20193 if ( ! $this->is_user_admin() ) {
20194 return;
20195 }
20196
20197 $action = fs_get_action();
20198
20199 if ( empty( $action ) ) {
20200 return;
20201 }
20202
20203 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
20204 $install_id = fs_request_get( 'install_id', '' );
20205
20206 // Alias.
20207 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
20208
20209 $is_network_action = $this->is_network_level_action();
20210 $blog_id = $this->is_network_level_site_specific_action();
20211
20212 if ( is_numeric( $blog_id ) ) {
20213 $this->switch_to_blog( $blog_id );
20214 } else {
20215 $blog_id = '';
20216 }
20217
20218 switch ( $action ) {
20219 case 'opt_in':
20220 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
20221
20222 if ( $plugin_id == $this->get_id() ) {
20223 if ( $is_network_action && ! empty( $blog_id ) ) {
20224 if ( ! $this->is_registered() ) {
20225 $this->install_with_user(
20226 $this->get_network_user(),
20227 false,
20228 false,
20229 false,
20230 false
20231 );
20232
20233 $this->_admin_notices->add(
20234 $this->get_text_inline( 'Site successfully opted in.', 'successful-opt-in' ),
20235 $this->get_text_inline( 'Awesome', 'awesome' )
20236 );
20237 }
20238 }
20239 }
20240 break;
20241
20242 case 'toggle_tracking':
20243 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
20244
20245 if ( $plugin_id == $this->get_id() ) {
20246 if ( $is_network_action && ! empty( $blog_id ) ) {
20247 if ( $this->is_registered() ) {
20248 if ( $this->is_tracking_prohibited() ) {
20249 if ( $this->allow_site_tracking() ) {
20250 $this->_admin_notices->add(
20251 sprintf( $this->get_text_inline( 'We appreciate your help in making the %s better by letting us track some usage data.', 'opt-out-message-appreciation' ), $this->_module_type ),
20252 $this->get_text_inline( 'Thank you!', 'thank-you' )
20253 );
20254 }
20255 } else {
20256 if ( $this->stop_site_tracking() ) {
20257 $this->_admin_notices->add(
20258 sprintf(
20259 $this->get_text_inline( 'We will no longer be sending any usage data of %s on %s to %s.', 'opted-out-successfully' ),
20260 $this->get_plugin_title(),
20261 fs_strip_url_protocol( get_site_url( $blog_id ) ),
20262 sprintf(
20263 '<a href="%s" target="_blank">%s</a>',
20264 'https://freemius.com',
20265 'freemius.com'
20266 )
20267 )
20268 );
20269 }
20270 }
20271 }
20272 }
20273 }
20274
20275 break;
20276
20277 case 'delete_account':
20278 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
20279
20280 if ( $plugin_id == $this->get_id() ) {
20281 if ( $is_network_action && empty( $blog_id ) ) {
20282 $this->delete_network_account_event();
20283 } else {
20284 $this->delete_account_event();
20285 }
20286
20287 // Clear user and site.
20288 $this->_site = null;
20289 $this->_user = null;
20290
20291 $this->maybe_set_slug_and_network_menu_exists_flag();
20292
20293 fs_redirect( $this->get_activation_url() );
20294 } else {
20295 if ( $this->is_addon_activated( $plugin_id ) ) {
20296 $fs_addon = self::get_instance_by_id( $plugin_id );
20297 $fs_addon->delete_account_event();
20298
20299 fs_redirect( $this->_get_admin_page_url( 'account' ) );
20300 }
20301 }
20302
20303 return;
20304
20305 case 'downgrade_account':
20306 if ( is_numeric( $blog_id ) ) {
20307 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
20308 } else {
20309 check_admin_referer( $action );
20310 }
20311
20312 $switch_to_network_install_blog_after_cancellation = (
20313 is_numeric( $blog_id ) &&
20314 $plugin_id == $this->get_id() &&
20315 ! $this->is_trial()
20316 );
20317
20318 $result = $this->cancel_subscription_or_trial( $plugin_id );
20319 if ( $this->is_api_error( $result ) ) {
20320 $this->_admin_notices->add(
20321 $result->error->message,
20322 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
20323 'error'
20324 );
20325 }
20326
20327 if ( $switch_to_network_install_blog_after_cancellation ) {
20328 $this->switch_to_blog( $this->_storage->network_install_blog_id );
20329 }
20330
20331 return;
20332
20333 case 'activate_license':
20334 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
20335
20336 $fs = $this;
20337 if ( $plugin_id != $this->get_id() ) {
20338 $fs = $this->is_addon_activated( $plugin_id ) ?
20339 self::get_instance_by_id( $plugin_id ) :
20340 null;
20341 }
20342
20343 if ( is_object( $fs ) ) {
20344 $fs->_activate_license();
20345 }
20346
20347 return;
20348
20349 case 'deactivate_license':
20350 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
20351
20352 if ( $plugin_id == $this->get_id() ) {
20353 $this->_deactivate_license();
20354
20355 if ( $this->is_only_premium() ) {
20356 // Clear user and site.
20357 $this->_site = null;
20358 $this->_user = null;
20359
20360 if ( ! $is_network_action ) {
20361 fs_redirect( $this->get_activation_url() );
20362 } else if ( is_numeric( $blog_id ) ) {
20363 $this->switch_to_blog( $this->_storage->network_install_blog_id );
20364 }
20365 }
20366 } else {
20367 if ( $this->is_addon_activated( $plugin_id ) ) {
20368 $fs_addon = self::get_instance_by_id( $plugin_id );
20369 $fs_addon->_deactivate_license();
20370 }
20371 }
20372
20373 return;
20374
20375 case 'check_updates':
20376 check_admin_referer( $action );
20377 $this->check_updates();
20378
20379 return;
20380
20381 case 'change_owner':
20382 $state = fs_request_get( 'state', 'init' );
20383 switch ( $state ) {
20384 case 'init':
20385 $candidate_email = fs_request_get( 'candidate_email', '' );
20386
20387 if ( $this->init_change_owner( $candidate_email ) ) {
20388 $this->_admin_notices->add( sprintf( $this->get_text_inline( 'Please check your mailbox, you should receive an email via %s to confirm the ownership change. From security reasons, you must confirm the change within the next 15 min. If you cannot find the email, please check your spam folder.', 'change-owner-request-sent-x' ), '<b>' . $this->_user->email . '</b>' ) );
20389 }
20390 break;
20391 case 'owner_confirmed':
20392 $candidate_email = fs_request_get( 'candidate_email', '' );
20393
20394 $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>' ) );
20395 break;
20396 case 'candidate_confirmed':
20397 if ( $this->complete_change_owner() ) {
20398 $this->_admin_notices->add_sticky(
20399 sprintf( $this->get_text_inline( '%s is the new owner of the account.', 'change-owner-request_candidate-confirmed' ), '<b>' . $this->_user->email . '</b>' ),
20400 'ownership_changed',
20401 $this->get_text_x_inline( 'Congrats', 'as congratulations', 'congrats' ) . '!'
20402 );
20403 } else {
20404 // @todo Handle failed ownership change message.
20405 }
20406 break;
20407 }
20408
20409 return;
20410
20411 case 'update_email':
20412 check_admin_referer( 'update_email' );
20413
20414 $new_email = fs_request_get( 'fs_email_' . $this->get_unique_affix(), '' );
20415 $result = $this->update_email( $new_email );
20416
20417 if ( isset( $result->error ) ) {
20418 switch ( $result->error->code ) {
20419 case 'user_exist':
20420 $this->_admin_notices->add(
20421 $this->get_text_inline( 'Sorry, we could not complete the email update. Another user with the same email is already registered.', 'user-exist-message' ) . ' ' .
20422 sprintf( $this->get_text_inline( 'If you would like to give up the ownership of the %s\'s account to %s click the Change Ownership button.', 'user-exist-message_ownership' ), $this->_module_type, '<b>' . $new_email . '</b>' ) .
20423 sprintf(
20424 '<a style="margin-left: 10px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
20425 $this->get_account_url( 'change_owner', array(
20426 'state' => 'init',
20427 'candidate_email' => $new_email
20428 ) ),
20429 $this->get_text_inline( 'Change Ownership', 'change-ownership' )
20430 ),
20431 $oops_text,
20432 'error'
20433 );
20434 break;
20435 }
20436 } else {
20437 $this->_admin_notices->add( $this->get_text_inline( 'Your email was successfully updated. You should receive an email with confirmation instructions in few moments.', 'email-updated-message' ) );
20438 }
20439
20440 return;
20441
20442 case 'update_user_name':
20443 check_admin_referer( 'update_user_name' );
20444
20445 $result = $this->update_user_name();
20446
20447 if ( isset( $result->error ) ) {
20448 $this->_admin_notices->add(
20449 $this->get_text_inline( 'Please provide your full name.', 'name-update-failed-message' ),
20450 $oops_text,
20451 'error'
20452 );
20453 } else {
20454 $this->_admin_notices->add( $this->get_text_inline( 'Your name was successfully updated.', 'name-updated-message' ) );
20455 }
20456
20457 return;
20458
20459 #region Actions that might be called from external links (e.g. email)
20460
20461 case 'cancel_trial':
20462 $result = $this->cancel_subscription_or_trial( $plugin_id );
20463 if ( $this->is_api_error( $result ) ) {
20464 $this->_admin_notices->add(
20465 $result->error->message,
20466 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
20467 'error'
20468 );
20469 }
20470
20471 return;
20472
20473 case 'verify_email':
20474 $this->verify_email();
20475
20476 return;
20477
20478 case 'sync_user':
20479 $this->_handle_account_user_sync();
20480
20481 return;
20482
20483 case $this->get_unique_affix() . '_sync_license':
20484 $this->_sync_license();
20485
20486 return;
20487
20488 case 'download_latest':
20489 $this->download_latest_directly( $plugin_id );
20490
20491 return;
20492
20493 #endregion
20494 }
20495
20496 if ( WP_FS__IS_POST_REQUEST ) {
20497 $properties = array( 'site_secret_key', 'site_id', 'site_public_key' );
20498 foreach ( $properties as $p ) {
20499 if ( 'update_' . $p === $action ) {
20500 check_admin_referer( $action );
20501
20502 $this->_logger->log( $action );
20503
20504 $site_property = substr( $p, strlen( 'site_' ) );
20505 $site_property_value = fs_request_get( 'fs_' . $p . '_' . $this->get_unique_affix(), '' );
20506 $this->get_site()->{$site_property} = $site_property_value;
20507
20508 // Store account after modification.
20509 $this->_store_site();
20510
20511 $this->do_action( 'account_property_edit', 'site', $site_property, $site_property_value );
20512
20513 $this->_admin_notices->add( sprintf(
20514 /* translators: %s: User's account property (e.g. email address, name) */
20515 $this->get_text_inline( 'You have successfully updated your %s.', 'x-updated' ),
20516 '<b>' . str_replace( '_', ' ', $p ) . '</b>'
20517 ) );
20518
20519 return;
20520 }
20521 }
20522 }
20523 }
20524
20525 /**
20526 * Account page resources load.
20527 *
20528 * @author Vova Feldman (@svovaf)
20529 * @since 1.0.6
20530 */
20531 function _account_page_load() {
20532 $this->_logger->entrance();
20533
20534 $this->_logger->info( var_export( $_REQUEST, true ) );
20535
20536 fs_enqueue_local_style( 'fs_account', '/admin/account.css' );
20537
20538 if ( $this->has_addons() ) {
20539 wp_enqueue_script( 'plugin-install' );
20540 add_thickbox();
20541
20542 function fs_addons_body_class( $classes ) {
20543 $classes .= ' plugins-php';
20544
20545 return $classes;
20546 }
20547
20548 add_filter( 'admin_body_class', 'fs_addons_body_class' );
20549 }
20550
20551 if ( $this->has_paid_plan() &&
20552 ! $this->has_any_license() &&
20553 ! $this->is_sync_executed() &&
20554 $this->is_tracking_allowed()
20555 ) {
20556 /**
20557 * If no licenses found and no sync job was executed during the last 24 hours,
20558 * just execute the sync job right away (blocking execution).
20559 *
20560 * @since 1.1.7.3
20561 */
20562 $this->run_manual_sync();
20563 }
20564
20565 $this->_handle_account_edits();
20566
20567 $this->do_action( 'account_page_load_before_departure' );
20568 }
20569
20570 /**
20571 * Renders the "Affiliation" page.
20572 *
20573 * @author Leo Fajardo (@leorw)
20574 * @since 1.2.3
20575 */
20576 function _affiliation_page_render() {
20577 $this->_logger->entrance();
20578
20579 $this->fetch_affiliate_and_terms();
20580
20581 fs_enqueue_local_style( 'fs_affiliation', '/admin/affiliation.css' );
20582
20583 $vars = array( 'id' => $this->_module_id );
20584 echo $this->apply_filters( "/forms/affiliation.php", fs_get_template( '/forms/affiliation.php', $vars ) );
20585 }
20586
20587
20588 /**
20589 * Render account page.
20590 *
20591 * @author Vova Feldman (@svovaf)
20592 * @since 1.0.0
20593 */
20594 function _account_page_render() {
20595 $this->_logger->entrance();
20596
20597 $template = 'account.php';
20598 $vars = array( 'id' => $this->_module_id );
20599
20600 /**
20601 * Added filter to the template to allow developers wrapping the template
20602 * in custom HTML (e.g. within a wizard/tabs).
20603 *
20604 * @author Vova Feldman (@svovaf)
20605 * @since 1.2.1.6
20606 */
20607 echo $this->apply_filters( "templates/{$template}", fs_get_template( $template, $vars ) );
20608 }
20609
20610 /**
20611 * Render account connect page.
20612 *
20613 * @author Vova Feldman (@svovaf)
20614 * @since 1.0.7
20615 */
20616 function _connect_page_render() {
20617 $this->_logger->entrance();
20618
20619 $vars = array( 'id' => $this->_module_id );
20620
20621 /**
20622 * Added filter to the template to allow developers wrapping the template
20623 * in custom HTML (e.g. within a wizard/tabs).
20624 *
20625 * @author Vova Feldman (@svovaf)
20626 * @since 1.2.1.6
20627 */
20628 echo $this->apply_filters( 'templates/connect.php', fs_get_template( 'connect.php', $vars ) );
20629 }
20630
20631 /**
20632 * Load required resources before add-ons page render.
20633 *
20634 * @author Vova Feldman (@svovaf)
20635 * @since 1.0.6
20636 */
20637 function _addons_page_load() {
20638 $this->_logger->entrance();
20639
20640 fs_enqueue_local_style( 'fs_addons', '/admin/add-ons.css' );
20641
20642 wp_enqueue_script( 'plugin-install' );
20643 add_thickbox();
20644
20645 function fs_addons_body_class( $classes ) {
20646 $classes .= ' plugins-php';
20647
20648 return $classes;
20649 }
20650
20651 add_filter( 'admin_body_class', 'fs_addons_body_class' );
20652
20653 if ( ! $this->is_registered() && $this->is_org_repo_compliant() ) {
20654 $this->_admin_notices->add(
20655 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>' ),
20656 $this->get_text_x_inline( 'Heads up', 'advance notice of something that will need attention.', 'heads-up' ),
20657 'update-nag'
20658 );
20659 }
20660 }
20661
20662 /**
20663 * Render add-ons page.
20664 *
20665 * @author Vova Feldman (@svovaf)
20666 * @since 1.0.6
20667 */
20668 function _addons_page_render() {
20669 $this->_logger->entrance();
20670
20671 $vars = array( 'id' => $this->_module_id );
20672
20673 /**
20674 * Added filter to the template to allow developers wrapping the template
20675 * in custom HTML (e.g. within a wizard/tabs).
20676 *
20677 * @author Vova Feldman (@svovaf)
20678 * @since 1.2.1.6
20679 */
20680 echo $this->apply_filters( 'templates/add-ons.php', fs_get_template( 'add-ons.php', $vars ) );
20681 }
20682
20683 /* Pricing & Upgrade
20684 ------------------------------------------------------------------------------------------------------------------*/
20685 /**
20686 * Render pricing page.
20687 *
20688 * @author Vova Feldman (@svovaf)
20689 * @since 1.0.0
20690 */
20691 function _pricing_page_render() {
20692 $this->_logger->entrance();
20693
20694 $vars = array( 'id' => $this->_module_id );
20695
20696 if ( 'true' === fs_request_get( 'checkout', false ) ) {
20697 echo $this->apply_filters( 'templates/checkout.php', fs_get_template( 'checkout.php', $vars ) );
20698 } else {
20699 echo $this->apply_filters( 'templates/pricing.php', fs_get_template( 'pricing.php', $vars ) );
20700 }
20701 }
20702
20703 #----------------------------------------------------------------------------------
20704 #region Contact Us
20705 #----------------------------------------------------------------------------------
20706
20707 /**
20708 * Render contact-us page.
20709 *
20710 * @author Vova Feldman (@svovaf)
20711 * @since 1.0.3
20712 */
20713 function _contact_page_render() {
20714 $this->_logger->entrance();
20715
20716 $vars = array( 'id' => $this->_module_id );
20717
20718 /**
20719 * Added filter to the template to allow developers wrapping the template
20720 * in custom HTML (e.g. within a wizard/tabs).
20721 *
20722 * @author Vova Feldman (@svovaf)
20723 * @since 2.1.3
20724 */
20725 echo $this->apply_filters( 'templates/contact.php', fs_get_template( 'contact.php', $vars ) );
20726 }
20727
20728 #endregion ------------------------------------------------------------------------
20729
20730 /**
20731 * Hide all admin notices to prevent distractions.
20732 *
20733 * @author Vova Feldman (@svovaf)
20734 * @since 1.0.3
20735 *
20736 * @uses remove_all_actions()
20737 */
20738 private static function _hide_admin_notices() {
20739 remove_all_actions( 'admin_notices' );
20740 remove_all_actions( 'network_admin_notices' );
20741 remove_all_actions( 'all_admin_notices' );
20742 remove_all_actions( 'user_admin_notices' );
20743 }
20744
20745 static function _clean_admin_content_section_hook() {
20746 self::_hide_admin_notices();
20747
20748 // Hide footer.
20749 echo '<style>#wpfooter { display: none !important; }</style>';
20750 }
20751
20752 /**
20753 * Attach to admin_head hook to hide all admin notices.
20754 *
20755 * @author Vova Feldman (@svovaf)
20756 * @since 1.0.3
20757 */
20758 static function _clean_admin_content_section() {
20759 add_action( 'admin_head', 'Freemius::_clean_admin_content_section_hook' );
20760 }
20761
20762 /* CSS & JavaScript
20763 ------------------------------------------------------------------------------------------------------------------*/
20764 /* function _enqueue_script($handle, $src) {
20765 $url = plugins_url( substr( WP_FS__DIR_JS, strlen( $this->_plugin_dir_path ) ) . '/assets/js/' . $src );
20766
20767 $this->_logger->entrance( 'script = ' . $url );
20768
20769 wp_enqueue_script( $handle, $url );
20770 }*/
20771
20772 /* SDK
20773 ------------------------------------------------------------------------------------------------------------------*/
20774 private $_user_api;
20775
20776 /**
20777 *
20778 * @author Vova Feldman (@svovaf)
20779 * @since 1.0.2
20780 *
20781 * @param bool $flush
20782 *
20783 * @return FS_Api
20784 */
20785 private function get_api_user_scope( $flush = false ) {
20786 if ( ! isset( $this->_user_api ) || $flush ) {
20787 $this->_user_api = $this->get_api_user_scope_by_user( $this->_user );
20788 }
20789
20790 return $this->_user_api;
20791 }
20792
20793 /**
20794 * @author Vova Feldman (@svovaf)
20795 * @since 2.0.0
20796 *
20797 * @param \FS_User $user
20798 *
20799 * @return \FS_Api
20800 */
20801 private function get_api_user_scope_by_user( FS_User $user ) {
20802 return FS_Api::instance(
20803 $this->_module_id,
20804 'user',
20805 $user->id,
20806 $user->public_key,
20807 ! $this->is_live(),
20808 $user->secret_key,
20809 $this->get_sdk_version()
20810 );
20811 }
20812
20813 /**
20814 *
20815 * @author Leo Fajardo (@leorw)
20816 * @since 2.0.0
20817 *
20818 * @param bool $flush
20819 *
20820 * @return FS_Api
20821 */
20822 private function get_current_or_network_user_api_scope( $flush = false ) {
20823 if ( ! $this->_is_network_active ||
20824 ( isset( $this->_user ) && $this->_user instanceof FS_User )
20825 ) {
20826 return $this->get_api_user_scope( $flush );
20827 }
20828
20829 $user = $this->get_current_or_network_user();
20830
20831 $this->_user_api = FS_Api::instance(
20832 $this->_module_id,
20833 'user',
20834 $user->id,
20835 $user->public_key,
20836 ! $this->is_live(),
20837 $user->secret_key,
20838 $this->get_sdk_version()
20839 );
20840
20841 return $this->_user_api;
20842 }
20843
20844 private $_site_api;
20845
20846 /**
20847 *
20848 * @author Vova Feldman (@svovaf)
20849 * @since 1.0.2
20850 *
20851 * @param bool $flush
20852 *
20853 * @return FS_Api
20854 */
20855 private function get_api_site_scope( $flush = false ) {
20856 if ( ! isset( $this->_site_api ) || $flush ) {
20857 $this->_site_api = FS_Api::instance(
20858 $this->_module_id,
20859 'install',
20860 $this->_site->id,
20861 $this->_site->public_key,
20862 ! $this->is_live(),
20863 $this->_site->secret_key,
20864 $this->get_sdk_version()
20865 );
20866 }
20867
20868 return $this->_site_api;
20869 }
20870
20871 private $_plugin_api;
20872
20873 /**
20874 * Get plugin public API scope.
20875 *
20876 * @author Vova Feldman (@svovaf)
20877 * @since 1.0.7
20878 *
20879 * @return FS_Api
20880 */
20881 function get_api_plugin_scope() {
20882 if ( ! isset( $this->_plugin_api ) ) {
20883 $this->_plugin_api = FS_Api::instance(
20884 $this->_module_id,
20885 'plugin',
20886 $this->_plugin->id,
20887 $this->_plugin->public_key,
20888 ! $this->is_live(),
20889 false,
20890 $this->get_sdk_version()
20891 );
20892 }
20893
20894 return $this->_plugin_api;
20895 }
20896
20897 /**
20898 * Get site API scope object (fallback to public plugin scope when not registered).
20899 *
20900 * @author Vova Feldman (@svovaf)
20901 * @since 1.0.7
20902 *
20903 * @return FS_Api
20904 */
20905 function get_api_site_or_plugin_scope() {
20906 return $this->is_registered() ?
20907 $this->get_api_site_scope() :
20908 $this->get_api_plugin_scope();
20909 }
20910
20911 /**
20912 * Show trial promotional notice (if any trial exist).
20913 *
20914 * @author Vova Feldman (@svovaf)
20915 * @since 1.0.9
20916 *
20917 * @param FS_Plugin_Plan[] $plans
20918 */
20919 function _check_for_trial_plans( $plans ) {
20920 /**
20921 * 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.
20922 *
20923 * @author Vova Feldman (@svovaf)
20924 * @since 2.1.2
20925 */
20926 if ( ! is_array( $plans ) && is_object( $plans ) ) {
20927 $plans = array( $plans );
20928 }
20929
20930 if ( ! $this->is_array_instanceof( $plans, 'FS_Plugin_Plan' ) ) {
20931 $plans = array();
20932 }
20933
20934 $this->_storage->has_trial_plan = FS_Plan_Manager::instance()->has_trial_plan( $plans );
20935 }
20936
20937 /**
20938 * During trial promotion the "upgrade" submenu item turns to
20939 * "start trial" to encourage the trial. Since we want to keep
20940 * the same menu item handler and there's no robust way to
20941 * add new arguments to the menu item link's querystring,
20942 * use JavaScript to find the menu item and update the href of
20943 * the link.
20944 *
20945 * @author Vova Feldman (@svovaf)
20946 * @since 1.2.1.5
20947 */
20948 function _fix_start_trial_menu_item_url() {
20949 $template_args = array( 'id' => $this->_module_id );
20950 fs_require_template( 'add-trial-to-pricing.php', $template_args );
20951 }
20952
20953 /**
20954 * Check if module is currently in a trial promotion mode.
20955 *
20956 * @author Vova Feldman (@svovaf)
20957 * @since 1.2.2.7
20958 *
20959 * @return bool
20960 */
20961 function is_in_trial_promotion() {
20962 return $this->_admin_notices->has_sticky( 'trial_promotion' );
20963 }
20964
20965 /**
20966 * Show trial promotional notice (if any trial exist).
20967 *
20968 * @author Vova Feldman (@svovaf)
20969 * @since 1.0.9
20970 *
20971 * @return bool If trial notice added.
20972 */
20973 function _add_trial_notice() {
20974 if ( ! $this->is_user_admin() ) {
20975 return false;
20976 }
20977
20978 if ( ! $this->is_user_in_admin() ) {
20979 return false;
20980 }
20981
20982 if ( $this->_is_network_active ) {
20983 if ( fs_is_network_admin() ) {
20984 // Network level trial is disabled at the moment.
20985 return false;
20986 }
20987
20988 if ( ! $this->is_delegated_connection() ) {
20989 // Only delegated sites should support trials.
20990 return false;
20991 }
20992 }
20993
20994 // Check if trial message is already shown.
20995 if ( $this->is_in_trial_promotion() ) {
20996 add_action( 'admin_footer', array( &$this, '_fix_start_trial_menu_item_url' ) );
20997
20998 $this->_menu->add_counter_to_menu_item( 1, 'fs-trial' );
20999
21000 return false;
21001 }
21002
21003 if ( $this->is_premium() && ! WP_FS__DEV_MODE ) {
21004 // Don't show trial if running the premium code, unless running in DEV mode.
21005 return false;
21006 }
21007
21008 if ( ! $this->has_trial_plan() ) {
21009 // No plans with trial.
21010 return false;
21011 }
21012
21013 if ( ! $this->apply_filters( 'show_trial', true ) ) {
21014 // Developer explicitly asked not to show the trial promo.
21015 return false;
21016 }
21017
21018 if ( $this->is_registered() ) {
21019 // Check if trial already utilized.
21020 if ( $this->_site->is_trial_utilized() ) {
21021 return false;
21022 }
21023
21024 if ( $this->is_paying_or_trial() ) {
21025 // Don't show trial if paying or already in trial.
21026 return false;
21027 }
21028 }
21029
21030 if ( $this->is_activation_mode() || $this->is_pending_activation() ) {
21031 // If not yet opted-in/skipped, or pending activation, don't show trial.
21032 return false;
21033 }
21034
21035 $last_time_trial_promotion_shown = $this->_storage->get( 'trial_promotion_shown', false );
21036 $was_promotion_shown_before = ( false !== $last_time_trial_promotion_shown );
21037
21038 // Show promotion if never shown before and 24 hours after initial activation with FS.
21039 if ( ! $was_promotion_shown_before &&
21040 $this->_storage->install_timestamp > ( time() - $this->apply_filters( 'show_first_trial_after_n_sec', WP_FS__TIME_24_HOURS_IN_SEC ) )
21041 ) {
21042 return false;
21043 }
21044
21045 // OR if promotion was shown before, try showing it every 30 days.
21046 if ( $was_promotion_shown_before &&
21047 $this->apply_filters( 'reshow_trial_after_every_n_sec', 30 * WP_FS__TIME_24_HOURS_IN_SEC ) > time() - $last_time_trial_promotion_shown
21048 ) {
21049 return false;
21050 }
21051
21052 $trial_period = $this->_trial_days;
21053 $require_payment = $this->_is_trial_require_payment;
21054 $trial_url = $this->get_trial_url();
21055 $plans_string = strtolower( $this->get_text_inline( 'Awesome', 'awesome' ) );
21056
21057 if ( $this->is_registered() ) {
21058 // If opted-in, override trial with up to date data from API.
21059 $trial_plans = FS_Plan_Manager::instance()->get_trial_plans( $this->_plans );
21060 $trial_plans_count = count( $trial_plans );
21061
21062 if ( 0 === $trial_plans_count ) {
21063 // If there's no plans with a trial just exit.
21064 return false;
21065 }
21066
21067 /**
21068 * @var FS_Plugin_Plan $paid_plan
21069 */
21070 $paid_plan = $trial_plans[0];
21071 $require_payment = $paid_plan->is_require_subscription;
21072 $trial_period = $paid_plan->trial_period;
21073
21074 $total_paid_plans = count( $this->_plans ) - ( FS_Plan_Manager::instance()->has_free_plan( $this->_plans ) ? 1 : 0 );
21075
21076 if ( $total_paid_plans !== $trial_plans_count ) {
21077 // Not all paid plans have a trial - generate a string of those that have it.
21078 for ( $i = 0; $i < $trial_plans_count; $i ++ ) {
21079 $plans_string .= sprintf(
21080 ' <a href="%s">%s</a>',
21081 $trial_url,
21082 $trial_plans[ $i ]->title
21083 );
21084
21085 if ( $i < $trial_plans_count - 2 ) {
21086 $plans_string .= ', ';
21087 } else if ( $i == $trial_plans_count - 2 ) {
21088 $plans_string .= ' and ';
21089 }
21090 }
21091 }
21092 }
21093
21094 $message = sprintf(
21095 $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' ),
21096 sprintf( '<b>%s</b>', $this->get_plugin_name() ),
21097 $plans_string,
21098 $trial_period
21099 );
21100
21101 // "No Credit-Card Required" or "No Commitment for N Days".
21102 $cc_string = $require_payment ?
21103 sprintf( $this->get_text_inline( 'No commitment for %s days - cancel anytime!', 'no-commitment-for-x-days' ), $trial_period ) :
21104 $this->get_text_inline( 'No credit card required', 'no-cc-required' ) . '!';
21105
21106
21107 // Start trial button.
21108 $button = ' ' . sprintf(
21109 '<a style="margin-left: 10px; vertical-align: super;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
21110 $trial_url,
21111 $this->get_text_x_inline( 'Start free trial', 'call to action', 'start-free-trial' )
21112 );
21113
21114 $this->_admin_notices->add_sticky(
21115 $this->apply_filters( 'trial_promotion_message', "{$message} {$cc_string} {$button}" ),
21116 'trial_promotion',
21117 '',
21118 'promotion'
21119 );
21120
21121 $this->_storage->trial_promotion_shown = WP_FS__SCRIPT_START_TIME;
21122
21123 return true;
21124 }
21125
21126 /**
21127 * Lets users/customers know that the product has an affiliate program.
21128 *
21129 * @author Leo Fajardo (@leorw)
21130 * @since 1.2.2.11
21131 *
21132 * @return bool Returns true if the notice has been added.
21133 */
21134 function _add_affiliate_program_notice() {
21135 if ( ! $this->is_user_admin() ) {
21136 return false;
21137 }
21138
21139 if ( ! $this->is_user_in_admin() ) {
21140 return false;
21141 }
21142
21143 // Check if the notice is already shown.
21144 if ( $this->_admin_notices->has_sticky( 'affiliate_program' ) ) {
21145 return false;
21146 }
21147
21148 if (
21149 // Product has no affiliate program.
21150 ! $this->has_affiliate_program() ||
21151 // User has applied for an affiliate account.
21152 ! empty( $this->_storage->affiliate_application_data )
21153 ) {
21154 return false;
21155 }
21156
21157 if ( ! $this->apply_filters( 'show_affiliate_program_notice', true ) ) {
21158 // Developer explicitly asked not to show the notice about the affiliate program.
21159 return false;
21160 }
21161
21162 if ( $this->is_activation_mode() || $this->is_pending_activation() ) {
21163 // If not yet opted in/skipped, or pending activation, don't show the notice.
21164 return false;
21165 }
21166
21167 $last_time_notice_was_shown = $this->_storage->get( 'affiliate_program_notice_shown', false );
21168 $was_notice_shown_before = ( false !== $last_time_notice_was_shown );
21169
21170 /**
21171 * Do not show the notice if it was already shown before or less than 30 days have passed since the initial
21172 * activation with FS.
21173 */
21174 if ( $was_notice_shown_before ||
21175 $this->_storage->install_timestamp > ( time() - ( WP_FS__TIME_24_HOURS_IN_SEC * 30 ) )
21176 ) {
21177 return false;
21178 }
21179
21180 if ( ! $this->is_paying() &&
21181 FS_Plugin::AFFILIATE_MODERATION_CUSTOMERS == $this->_plugin->affiliate_moderation
21182 ) {
21183 // If the user is not a customer and the affiliate program is only for customers, don't show the notice.
21184 return false;
21185 }
21186
21187 $message = sprintf(
21188 $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' ),
21189 sprintf( '<strong>%s</strong>', $this->get_plugin_name() ),
21190 $this->get_module_label( true )
21191 );
21192
21193 // HTML code for the "Learn more..." button.
21194 $button = ' ' . sprintf(
21195 '<a style="display: block; margin-top: 10px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
21196 $this->_get_admin_page_url( 'affiliation' ),
21197 $this->get_text_inline( 'Learn more', 'learn-more' ) . '...'
21198 );
21199
21200 $this->_admin_notices->add_sticky(
21201 $this->apply_filters( 'affiliate_program_notice', "{$message} {$button}" ),
21202 'affiliate_program',
21203 '',
21204 'promotion'
21205 );
21206
21207 $this->_storage->affiliate_program_notice_shown = WP_FS__SCRIPT_START_TIME;
21208
21209 return true;
21210 }
21211
21212 /**
21213 * @author Vova Feldman (@svovaf)
21214 * @since 1.2.1.5
21215 */
21216 function _enqueue_common_css() {
21217 if ( $this->has_paid_plan() && ! $this->is_paying() ) {
21218 // Add basic CSS for admin-notices and menu-item colors.
21219 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
21220 }
21221 }
21222
21223 /**
21224 * @author Leo Fajardo (@leorw)
21225 * @since 1.2.2
21226 */
21227 function _show_theme_activation_optin_dialog() {
21228 fs_enqueue_local_style( 'fs_connect', '/admin/connect.css' );
21229
21230 add_action( 'admin_footer-themes.php', array( &$this, '_add_fs_theme_activation_dialog' ) );
21231 }
21232
21233 /**
21234 * @author Leo Fajardo (@leorw)
21235 * @since 1.2.2
21236 */
21237 function _add_fs_theme_activation_dialog() {
21238 $vars = array( 'id' => $this->_module_id );
21239 fs_require_once_template( 'connect.php', $vars );
21240 }
21241
21242 /* Action Links
21243 ------------------------------------------------------------------------------------------------------------------*/
21244 private $_action_links_hooked = false;
21245 private $_action_links = array();
21246
21247 /**
21248 * Hook to plugin action links filter.
21249 *
21250 * @author Vova Feldman (@svovaf)
21251 * @since 1.0.0
21252 */
21253 private function hook_plugin_action_links() {
21254 $this->_logger->entrance();
21255
21256 $this->_action_links_hooked = true;
21257
21258 $this->_logger->log( 'Adding action links hooks.' );
21259
21260 // Add action link to settings page.
21261 add_filter( 'plugin_action_links_' . $this->_plugin_basename, array(
21262 &$this,
21263 '_modify_plugin_action_links_hook'
21264 ), WP_FS__DEFAULT_PRIORITY, 2 );
21265 add_filter( 'network_admin_plugin_action_links_' . $this->_plugin_basename, array(
21266 &$this,
21267 '_modify_plugin_action_links_hook'
21268 ), WP_FS__DEFAULT_PRIORITY, 2 );
21269 }
21270
21271 /**
21272 * Add plugin action link.
21273 *
21274 * @author Vova Feldman (@svovaf)
21275 * @since 1.0.0
21276 *
21277 * @param $label
21278 * @param $url
21279 * @param bool $external
21280 * @param int $priority
21281 * @param bool $key
21282 */
21283 function add_plugin_action_link( $label, $url, $external = false, $priority = WP_FS__DEFAULT_PRIORITY, $key = false ) {
21284 $this->_logger->entrance();
21285
21286 if ( ! isset( $this->_action_links[ $priority ] ) ) {
21287 $this->_action_links[ $priority ] = array();
21288 }
21289
21290 if ( false === $key ) {
21291 $key = preg_replace( "/[^A-Za-z0-9 ]/", '', strtolower( $label ) );
21292 }
21293
21294 $this->_action_links[ $priority ][] = array(
21295 'label' => $label,
21296 'href' => $url,
21297 'key' => $key,
21298 'external' => $external
21299 );
21300 }
21301
21302 /**
21303 * Adds Upgrade and Add-Ons links to the main Plugins page link actions collection.
21304 *
21305 * @author Vova Feldman (@svovaf)
21306 * @since 1.0.0
21307 */
21308 function _add_upgrade_action_link() {
21309 $this->_logger->entrance();
21310
21311 $is_activation_mode = $this->is_activation_mode();
21312
21313 $add_action_links = $this->should_add_submenu_or_action_links( $is_activation_mode );
21314
21315 /**
21316 * The following logic is based on the logic in `add_submenu_items()` method that decides when the "Upgrade"
21317 * and "Add-Ons" menus should be added.
21318 *
21319 * @author Leo Fajardo (@leorw)
21320 * @since 2.3.0
21321 */
21322 $add_upgrade_link = (
21323 $add_action_links ||
21324 ( $is_activation_mode && $this->is_only_premium() )
21325 ) && ! WP_FS__DEMO_MODE;
21326
21327 $add_addons_link = ( $add_action_links && $this->has_addons() );
21328
21329 if ( ! $add_upgrade_link && ! $add_addons_link ) {
21330 return;
21331 }
21332
21333 if (
21334 $add_upgrade_link &&
21335 $this->is_pricing_page_visible() &&
21336 $this->is_submenu_item_visible( 'pricing' )
21337 ) {
21338 $this->add_plugin_action_link(
21339 $this->get_text_inline( 'Upgrade', 'upgrade' ),
21340 $this->get_upgrade_url(),
21341 false,
21342 7,
21343 'upgrade'
21344 );
21345 }
21346
21347 if (
21348 $add_addons_link &&
21349 $this->has_addons() &&
21350 $this->is_submenu_item_visible( 'addons' )
21351 ) {
21352 $this->add_plugin_action_link(
21353 $this->get_text_inline( 'Add-Ons', 'add-ons' ),
21354 $this->_get_admin_page_url( 'addons' ),
21355 false,
21356 9,
21357 'addons'
21358 );
21359 }
21360 }
21361
21362 /**
21363 * Adds "Activate License" or "Change License" link to the main Plugins page link actions collection.
21364 *
21365 * @author Leo Fajardo (@leorw)
21366 * @since 1.1.9
21367 */
21368 function _add_license_action_link() {
21369 $this->_logger->entrance();
21370
21371 if ( ! self::is_ajax() ) {
21372 // Inject license activation dialog UI and client side code.
21373 add_action( 'admin_footer', array( &$this, '_add_license_activation_dialog_box' ) );
21374 }
21375
21376 $link_text = $this->is_free_plan() ?
21377 $this->get_text_inline( 'Activate License', 'activate-license' ) :
21378 $this->get_text_inline( 'Change License', 'change-license' );
21379
21380 $this->add_plugin_action_link(
21381 $link_text,
21382 '#',
21383 false,
21384 11,
21385 ( 'activate-license ' . $this->get_unique_affix() )
21386 );
21387 }
21388
21389 /**
21390 * @author Leo Fajardo (@leorw)
21391 * @since 2.0.2
21392 */
21393 function _add_premium_version_upgrade_selection_action() {
21394 $this->_logger->entrance();
21395
21396 if ( ! self::is_ajax() ) {
21397 add_action( 'admin_footer', array( &$this, '_add_premium_version_upgrade_selection_dialog_box' ) );
21398 }
21399 }
21400
21401 /**
21402 * Adds "Opt In" or "Opt Out" link to the main "Plugins" page link actions collection.
21403 *
21404 * @author Leo Fajardo (@leorw)
21405 * @since 1.2.1.5
21406 */
21407 function _add_tracking_links() {
21408 if ( ! current_user_can( 'manage_options' ) ) {
21409 return;
21410 }
21411
21412 $this->_logger->entrance();
21413
21414 if ( fs_is_network_admin() ) {
21415 if ( ! $this->_is_network_active ) {
21416 // Don't add tracking links when browsing the network WP Admin and the plugin is not network active.
21417 return;
21418 } else if ( $this->is_network_delegated_connection() ) {
21419 // Don't add tracking links when browsing the network WP Admin and the activation has been delegated to site admins.
21420 return;
21421 }
21422 } else {
21423 if ( $this->_is_network_active && ! $this->is_delegated_connection() ) {
21424 // Don't add tracking links when browsing the sub-site WP Admin, the plugin is network active, and the connection was not delegated.
21425 return;
21426 }
21427 }
21428
21429 if ( fs_request_is_action_secure( $this->get_unique_affix() . '_reconnect' ) ) {
21430 if ( ! $this->is_registered() && $this->is_anonymous() ) {
21431 $this->connect_again();
21432
21433 return;
21434 }
21435 }
21436
21437 if ( ( $this->is_plugin() && ! self::is_plugins_page() ) ||
21438 ( $this->is_theme() && ! self::is_themes_page() )
21439 ) {
21440 // Only show tracking links on the plugins and themes pages.
21441 return;
21442 }
21443
21444 if ( $this->is_registered() && $this->is_tracking_allowed() ) {
21445 if ( ! $this->is_enable_anonymous() ) {
21446 // If opted in and tracking is allowed, don't allow to opt out if anonymous mode is disabled.
21447 return;
21448 }
21449
21450 if ( ! $this->is_free_plan() ) {
21451 // Don't allow to opt out if running in paid plan.
21452 return;
21453 }
21454 }
21455
21456 if ( $this->add_ajax_action( 'stop_tracking', array( &$this, '_stop_tracking_callback' ) ) ) {
21457 return;
21458 }
21459
21460 if ( $this->add_ajax_action( 'allow_tracking', array( &$this, '_allow_tracking_callback' ) ) ) {
21461 return;
21462 }
21463
21464 $link_text_id = '';
21465 $url = '#';
21466
21467 if ( $this->is_registered() ) {
21468 if ( $this->is_tracking_allowed() ) {
21469 $link_text_id = $this->get_text_inline( 'Opt Out', 'opt-out' );
21470 } else {
21471 $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' );
21472 }
21473
21474 add_action( 'admin_footer', array( &$this, '_add_optout_dialog' ) );
21475 } else if ( $this->is_anonymous() || $this->is_activation_mode() ) {
21476 /**
21477 * Show opt-in link only if skipped or in activation mode.
21478 */
21479 $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' );
21480
21481 $params = ! $this->is_anonymous() ?
21482 array() :
21483 array(
21484 'nonce' => wp_create_nonce( $this->get_unique_affix() . '_reconnect' ),
21485 'fs_action' => ( $this->get_unique_affix() . '_reconnect' ),
21486 );
21487
21488 $url = $this->get_activation_url( $params );
21489 }
21490
21491 if ( ! empty( $link_text_id ) && $this->is_plugin() && self::is_plugins_page() ) {
21492 $this->add_plugin_action_link(
21493 $link_text_id,
21494 $url,
21495 false,
21496 13,
21497 "opt-in-or-opt-out {$this->_slug}"
21498 );
21499 }
21500 }
21501
21502 /**
21503 * Get the URL of the page that should be loaded right after the plugin activation.
21504 *
21505 * @author Vova Feldman (@svovaf)
21506 * @since 1.1.7.4
21507 *
21508 * @return string
21509 */
21510 function get_after_plugin_activation_redirect_url() {
21511 $url = false;
21512
21513 if ( ! $this->is_addon() || ! $this->has_free_plan() ) {
21514 $first_time_path = $this->_menu->get_first_time_path(
21515 fs_is_network_admin() && $this->_is_network_active
21516 );
21517
21518 if ( $this->is_activation_mode() ) {
21519 $url = $this->get_activation_url();
21520 } else if ( ! empty( $first_time_path ) ) {
21521 $url = $first_time_path;
21522 } else {
21523 $page = '';
21524 if ( ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
21525 if ( $this->is_network_registered() ) {
21526 $page = 'account';
21527 } else if ( $this->is_network_anonymous() ) {
21528 $this->maybe_set_slug_and_network_menu_exists_flag();
21529 }
21530 }
21531
21532 $url = $this->_get_admin_page_url( $page );
21533 }
21534 } else {
21535 $plugin_fs = false;
21536
21537 if ( $this->is_parent_plugin_installed() ) {
21538 $plugin_fs = self::get_parent_instance();
21539 }
21540
21541 if ( is_object( $plugin_fs ) ) {
21542 if ( ! $plugin_fs->is_registered() ) {
21543 // Forward to parent plugin connect when parent not registered.
21544 $url = $plugin_fs->get_activation_url();
21545 } else {
21546 // Forward to account page.
21547 $url = $plugin_fs->_get_admin_page_url( 'account' );
21548 }
21549 }
21550 }
21551
21552 return $url;
21553 }
21554
21555 /**
21556 * Forward page to activation page.
21557 *
21558 * @author Vova Feldman (@svovaf)
21559 * @since 1.0.3
21560 */
21561 function _redirect_on_activation_hook() {
21562 $url = $this->get_after_plugin_activation_redirect_url();
21563
21564 if ( is_string( $url ) ) {
21565 fs_redirect( $url );
21566 }
21567 }
21568
21569 /**
21570 * Modify plugin's page action links collection.
21571 *
21572 * @author Vova Feldman (@svovaf)
21573 * @since 1.0.0
21574 *
21575 * @param array $links
21576 * @param $file
21577 *
21578 * @return array
21579 */
21580 function _modify_plugin_action_links_hook( $links, $file ) {
21581 $this->_logger->entrance();
21582
21583 $passed_deactivate = false;
21584 $deactivate_link = '';
21585 $before_deactivate = array();
21586 $after_deactivate = array();
21587 foreach ( $links as $key => $link ) {
21588 if ( 'deactivate' === $key ) {
21589 $deactivate_link = $link;
21590 $passed_deactivate = true;
21591 continue;
21592 }
21593
21594 if ( ! $passed_deactivate ) {
21595 $before_deactivate[ $key ] = $link;
21596 } else {
21597 $after_deactivate[ $key ] = $link;
21598 }
21599 }
21600
21601 ksort( $this->_action_links );
21602
21603 foreach ( $this->_action_links as $new_links ) {
21604 foreach ( $new_links as $link ) {
21605 $before_deactivate[ $link['key'] ] = '<a href="' . $link['href'] . '"' . ( $link['external'] ? ' target="_blank"' : '' ) . '>' . $link['label'] . '</a>';
21606 }
21607 }
21608
21609 if ( ! empty( $deactivate_link ) ) {
21610 /**
21611 * This HTML element is used to identify the correct plugin when attaching an event to its Deactivate link.
21612 *
21613 * @since 1.2.1.6 Always show the deactivation feedback form since we added automatic free version deactivation upon premium code activation.
21614 */
21615 $deactivate_link .= '<i class="fs-module-id" data-module-id="' . $this->_module_id . '"></i>';
21616
21617 // Append deactivation link.
21618 $before_deactivate['deactivate'] = $deactivate_link;
21619 }
21620
21621 return array_merge( $before_deactivate, $after_deactivate );
21622 }
21623
21624 /**
21625 * Adds admin message.
21626 *
21627 * @author Vova Feldman (@svovaf)
21628 * @since 1.0.4
21629 *
21630 * @param string $message
21631 * @param string $title
21632 * @param string $type
21633 */
21634 function add_admin_message( $message, $title = '', $type = 'success' ) {
21635 $this->_admin_notices->add( $message, $title, $type );
21636 }
21637
21638 /**
21639 * Adds sticky admin message.
21640 *
21641 * @author Vova Feldman (@svovaf)
21642 * @since 1.1.0
21643 *
21644 * @param string $message
21645 * @param string $id
21646 * @param string $title
21647 * @param string $type
21648 */
21649 function add_sticky_admin_message( $message, $id, $title = '', $type = 'success' ) {
21650 $this->_admin_notices->add_sticky( $message, $id, $title, $type );
21651 }
21652
21653 /**
21654 * Check if the paid version of the module is installed.
21655 *
21656 * @author Vova Feldman (@svovaf)
21657 * @since 2.2.0
21658 *
21659 * @return bool
21660 */
21661 private function is_premium_version_installed() {
21662 $premium_plugin_basename = $this->premium_plugin_basename();
21663 $premium_plugin = get_plugins( '/' . dirname( $premium_plugin_basename ) );
21664
21665 return ! empty( $premium_plugin );
21666 }
21667
21668 /**
21669 * Helper function that returns the final steps for the upgrade completion.
21670 *
21671 * If the module is already running the premium code, returns an empty string.
21672 *
21673 * @author Vova Feldman (@svovaf)
21674 * @since 1.2.1
21675 *
21676 * @param string $plan_title
21677 *
21678 * @return string
21679 */
21680 private function get_complete_upgrade_instructions( $plan_title = '' ) {
21681 $this->_logger->entrance();
21682
21683 $activate_license_string = $this->get_license_network_activation_notice();
21684
21685 if ( ! $this->has_premium_version() || $this->is_premium() ) {
21686 return '' . $activate_license_string;
21687 }
21688
21689 if ( empty( $plan_title ) ) {
21690 $plan_title = $this->get_plan_title();
21691 }
21692
21693 if ( $this->is_premium_version_installed() ) {
21694 /**
21695 * If the premium version is already installed, instead of showing the installation instructions,
21696 * tell the current user to activate it.
21697 *
21698 * @author Leo Fajardo (@leorw)
21699 * @since 2.2.1
21700 */
21701 $premium_plugin_basename = $this->premium_plugin_basename();
21702
21703 return sprintf(
21704 /* translators: %1$s: Product title; %2$s: Plan title */
21705 $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' ),
21706 sprintf( '<em>%s</em>', esc_html( $this->get_plugin_title() ) ),
21707 $plan_title,
21708 sprintf(
21709 '<a style="margin-left: 10px;" href="%s"><button class="button button-primary">%s</button></a>',
21710 wp_nonce_url( 'plugins.php?action=activate&amp;plugin=' . $premium_plugin_basename, 'activate-plugin_' . $premium_plugin_basename ),
21711 esc_html( sprintf(
21712 /* translators: %s: Plan title */
21713 $this->get_text_inline( 'Activate %s features', 'activate-x-features' ),
21714 $plan_title
21715 ) )
21716 )
21717 );
21718 } else {
21719 // @since 1.2.1.5 The free version is auto deactivated.
21720 $deactivation_step = version_compare( $this->version, '1.2.1.5', '<' ) ?
21721 ( '<li>' . $this->esc_html_inline( 'Deactivate the free version', 'deactivate-free-version' ) . '.</li>' ) :
21722 '';
21723
21724 return sprintf(
21725 ' %s: <ol><li>%s.</li>%s<li>%s (<a href="%s" target="_blank">%s</a>).</li></ol>',
21726 $this->get_text_inline( 'Please follow these steps to complete the upgrade', 'follow-steps-to-complete-upgrade' ),
21727 ( empty( $activate_license_string ) ? '' : $activate_license_string . '</li><li>' ) .
21728 $this->get_latest_download_link( sprintf(
21729 /* translators: %s: Plan title */
21730 $this->get_text_inline( 'Download the latest %s version', 'download-latest-x-version' ),
21731 $plan_title
21732 ) ),
21733 $deactivation_step,
21734 $this->get_text_inline( 'Upload and activate the downloaded version', 'upload-and-activate' ),
21735 $this->apply_filters( 'upload_and_install_video_url', '//bit.ly/upload-wp-' . $this->_module_type . 's' ),
21736 $this->get_text_inline( 'How to upload and activate?', 'howto-upload-activate' )
21737 );
21738 }
21739 }
21740
21741 /**
21742 * @author Leo Fajardo (@leorw)
21743 * @since 2.1.0
21744 *
21745 * @param string $url
21746 * @param array $request
21747 */
21748 private static function enrich_request_for_debug( &$url, &$request ) {
21749 if ( WP_FS__DEBUG_SDK || isset( $_COOKIE['XDEBUG_SESSION'] ) ) {
21750 $url = add_query_arg( 'XDEBUG_SESSION_START', rand( 0, 9999999 ), $url );
21751 $url = add_query_arg( 'XDEBUG_SESSION', 'PHPSTORM', $url );
21752
21753 $request['cookies'] = array(
21754 new WP_Http_Cookie( array(
21755 'name' => 'XDEBUG_SESSION',
21756 'value' => 'PHPSTORM',
21757 ) )
21758 );
21759 }
21760 }
21761
21762 /**
21763 * @author Leo Fajardo (@leorw)
21764 * @since 2.1.0
21765 *
21766 * @param string $url
21767 * @param array $request
21768 * @param int $success_cache_expiration
21769 * @param int $failure_cache_expiration
21770 * @param bool $maybe_enrich_request_for_debug
21771 *
21772 * @return WP_Error|array
21773 */
21774 static function safe_remote_post(
21775 &$url,
21776 $request,
21777 $success_cache_expiration = 0,
21778 $failure_cache_expiration = 0,
21779 $maybe_enrich_request_for_debug = true
21780 ) {
21781 $should_cache = ($success_cache_expiration + $failure_cache_expiration > 0);
21782
21783 $cache_key = $should_cache ? md5( fs_strip_url_protocol($url) . json_encode( $request ) ) : false;
21784
21785 $response = (!WP_FS__DEBUG_SDK && ( false !== $cache_key )) ?
21786 get_transient( $cache_key ) :
21787 false;
21788
21789 if ( false === $response ) {
21790 if ( $maybe_enrich_request_for_debug ) {
21791 self::enrich_request_for_debug( $url, $request );
21792 }
21793
21794 $response = wp_remote_post( $url, $request );
21795
21796 if ( $response instanceof WP_Error ) {
21797 if ( 'https://' === substr( $url, 0, 8 ) &&
21798 isset( $response->errors ) &&
21799 isset( $response->errors['http_request_failed'] )
21800 ) {
21801 $http_error = strtolower( $response->errors['http_request_failed'][0] );
21802
21803 if ( false !== strpos( $http_error, 'ssl' ) ||
21804 false !== strpos( $http_error, 'curl error 35' )
21805 ) {
21806 // Failed due to old version of cURL or Open SSL (SSLv3 is not supported by CloudFlare).
21807 $url = 'http://' . substr( $url, 8 );
21808
21809 $request['timeout'] = 15;
21810 $response = wp_remote_post( $url, $request );
21811 }
21812 }
21813 }
21814
21815 if ( false !== $cache_key ) {
21816 set_transient(
21817 $cache_key,
21818 $response,
21819 ( ( $response instanceof WP_Error ) ?
21820 $failure_cache_expiration :
21821 $success_cache_expiration )
21822 );
21823 }
21824 }
21825
21826 return $response;
21827 }
21828
21829 /**
21830 * This method is used to enrich the after upgrade notice instructions when the upgraded
21831 * license cannot be activated network wide (license quota isn't large enough).
21832 *
21833 * @author Vova Feldman (@svovaf)
21834 * @since 2.0.0
21835 *
21836 * @return string
21837 */
21838 private function get_license_network_activation_notice() {
21839 if ( ! $this->_is_network_active ) {
21840 // Module isn't network level activated.
21841 return '';
21842 }
21843
21844 if ( ! fs_is_network_admin() ) {
21845 // Not network level admin.
21846 return '';
21847 }
21848
21849 if ( get_blog_count() == 1 ) {
21850 // There's only a single site in the network so if there's a context license it was already activated.
21851 return '';
21852 }
21853
21854 if ( ! is_object( $this->_license ) ) {
21855 // No context license.
21856 return '';
21857 }
21858
21859 if ( $this->_license->is_single_site() && 0 < $this->_license->activated ) {
21860 // 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).
21861 return '';
21862 }
21863
21864 if ( $this->can_activate_license_on_network( $this->_license ) ) {
21865 // 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).
21866 return '';
21867 }
21868
21869 return sprintf(
21870 $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' ),
21871 '<a href="' . $this->get_account_url( false, array( 'activate_license' => 'true' ) ) . '">',
21872 '</a>'
21873 );
21874 }
21875
21876 /**
21877 * @author Vova Feldman (@svovaf)
21878 * @since 1.2.1.7
21879 *
21880 * @param string $key
21881 *
21882 * @return string
21883 */
21884 function get_text( $key ) {
21885 return fs_text( $key, $this->_slug );
21886 }
21887
21888 /**
21889 * @author Vova Feldman (@svovaf)
21890 * @since 1.2.3
21891 *
21892 * @param string $text Translatable string.
21893 * @param string $key String key for overrides.
21894 *
21895 * @return string
21896 */
21897 function get_text_inline( $text, $key = '' ) {
21898 return _fs_text_inline( $text, $key, $this->_slug );
21899 }
21900
21901 /**
21902 * @author Vova Feldman (@svovaf)
21903 * @since 1.2.3
21904 *
21905 * @param string $text Translatable string.
21906 * @param string $context Context information for the translators.
21907 * @param string $key String key for overrides.
21908 *
21909 * @return string
21910 */
21911 function get_text_x_inline( $text, $context, $key ) {
21912 return _fs_text_x_inline( $text, $context, $key, $this->_slug );
21913 }
21914
21915 /**
21916 * @author Vova Feldman (@svovaf)
21917 * @since 1.2.3
21918 *
21919 * @param string $text Translatable string.
21920 * @param string $key String key for overrides.
21921 *
21922 * @return string
21923 */
21924 function esc_html_inline( $text, $key ) {
21925 return esc_html( _fs_text_inline( $text, $key, $this->_slug ) );
21926 }
21927
21928 #----------------------------------------------------------------------------------
21929 #region Versioning
21930 #----------------------------------------------------------------------------------
21931
21932 /**
21933 * Check if Freemius in SDK upgrade mode.
21934 *
21935 * @author Vova Feldman (@svovaf)
21936 * @since 1.0.9
21937 *
21938 * @return bool
21939 */
21940 function is_sdk_upgrade_mode() {
21941 return isset( $this->_storage->sdk_upgrade_mode ) ?
21942 $this->_storage->sdk_upgrade_mode :
21943 false;
21944 }
21945
21946 /**
21947 * Turn SDK upgrade mode off.
21948 *
21949 * @author Vova Feldman (@svovaf)
21950 * @since 1.0.9
21951 */
21952 function set_sdk_upgrade_complete() {
21953 $this->_storage->sdk_upgrade_mode = false;
21954 }
21955
21956 /**
21957 * Check if plugin upgrade mode.
21958 *
21959 * @author Vova Feldman (@svovaf)
21960 * @since 1.0.9
21961 *
21962 * @return bool
21963 */
21964 function is_plugin_upgrade_mode() {
21965 return isset( $this->_storage->plugin_upgrade_mode ) ?
21966 $this->_storage->plugin_upgrade_mode :
21967 false;
21968 }
21969
21970 /**
21971 * Turn plugin upgrade mode off.
21972 *
21973 * @author Vova Feldman (@svovaf)
21974 * @since 1.0.9
21975 */
21976 function set_plugin_upgrade_complete() {
21977 $this->_storage->plugin_upgrade_mode = false;
21978 }
21979
21980 #endregion
21981
21982 #----------------------------------------------------------------------------------
21983 #region Permissions
21984 #----------------------------------------------------------------------------------
21985
21986 /**
21987 * Check if specific permission requested.
21988 *
21989 * @author Vova Feldman (@svovaf)
21990 * @since 1.1.6
21991 *
21992 * @param string $permission
21993 *
21994 * @return bool
21995 */
21996 function is_permission_requested( $permission ) {
21997 return isset( $this->_permissions[ $permission ] ) && ( true === $this->_permissions[ $permission ] );
21998 }
21999
22000 #endregion
22001
22002 #----------------------------------------------------------------------------------
22003 #region Auto Activation
22004 #----------------------------------------------------------------------------------
22005
22006 /**
22007 * Hints the SDK if running an auto-installation.
22008 *
22009 * @var bool
22010 */
22011 private $_isAutoInstall = false;
22012
22013 /**
22014 * After upgrade callback to install and auto activate a plugin.
22015 * This code will only be executed on explicit request from the user,
22016 * following the practice Jetpack are using with their theme installations.
22017 *
22018 * @link https://make.wordpress.org/plugins/2017/03/16/clarification-of-guideline-8-executable-code-and-installs/
22019 *
22020 * @author Vova Feldman (@svovaf)
22021 * @since 1.2.1.7
22022 */
22023 function _install_premium_version_ajax_action() {
22024 $this->_logger->entrance();
22025
22026 $this->check_ajax_referer( 'install_premium_version' );
22027
22028 if ( ! $this->is_registered() ) {
22029 // Not registered.
22030 self::shoot_ajax_failure( array(
22031 'message' => $this->get_text_inline( 'Auto installation only works for opted-in users.', 'auto-install-error-not-opted-in' ),
22032 'code' => 'premium_installed',
22033 ) );
22034 }
22035
22036 $plugin_id = fs_request_get( 'target_module_id', $this->get_id() );
22037
22038 if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
22039 // Invalid ID.
22040 self::shoot_ajax_failure( array(
22041 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
22042 'code' => 'invalid_module_id',
22043 ) );
22044 }
22045
22046 if ( $plugin_id == $this->get_id() ) {
22047 if ( $this->is_premium() ) {
22048 // Already using the premium code version.
22049 self::shoot_ajax_failure( array(
22050 'message' => $this->get_text_inline( 'Premium version already active.', 'auto-install-error-premium-activated' ),
22051 'code' => 'premium_installed',
22052 ) );
22053 }
22054 if ( ! $this->can_use_premium_code() ) {
22055 // Don't have access to the premium code.
22056 self::shoot_ajax_failure( array(
22057 'message' => $this->get_text_inline( 'You do not have a valid license to access the premium version.', 'auto-install-error-invalid-license' ),
22058 'code' => 'invalid_license',
22059 ) );
22060 }
22061 if ( ! $this->has_release_on_freemius() ) {
22062 // Plugin is a serviceware, no premium code version.
22063 self::shoot_ajax_failure( array(
22064 'message' => $this->get_text_inline( 'Plugin is a "Serviceware" which means it does not have a premium code version.', 'auto-install-error-serviceware' ),
22065 'code' => 'premium_version_missing',
22066 ) );
22067 }
22068 } else {
22069 $addon = $this->get_addon( $plugin_id );
22070
22071 if ( ! is_object( $addon ) ) {
22072 // Invalid add-on ID.
22073 self::shoot_ajax_failure( array(
22074 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
22075 'code' => 'invalid_module_id',
22076 ) );
22077 }
22078
22079 if ( $this->is_addon_activated( $plugin_id, true ) ) {
22080 // Premium add-on version is already activated.
22081 self::shoot_ajax_failure( array(
22082 'message' => $this->get_text_inline( 'Premium add-on version already installed.', 'auto-install-error-premium-addon-activated' ),
22083 'code' => 'premium_installed',
22084 ) );
22085 }
22086 }
22087
22088 $this->_isAutoInstall = true;
22089
22090 // Try to install and activate.
22091 $updater = FS_Plugin_Updater::instance( $this );
22092 $result = $updater->install_and_activate_plugin( $plugin_id );
22093
22094 if ( is_array( $result ) && ! empty( $result['message'] ) ) {
22095 self::shoot_ajax_failure( array(
22096 'message' => $result['message'],
22097 'code' => $result['code'],
22098 ) );
22099 }
22100
22101 self::shoot_ajax_success( $result );
22102 }
22103
22104 /**
22105 * Displays module activation dialog box after a successful upgrade
22106 * where the user explicitly requested to auto download and install
22107 * the premium version.
22108 *
22109 * @author Vova Feldman (@svovaf)
22110 * @since 1.2.1.7
22111 */
22112 function _add_auto_installation_dialog_box() {
22113 $this->_logger->entrance();
22114
22115 if ( ! $this->is_registered() ) {
22116 // Not registered.
22117 return;
22118 }
22119
22120 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
22121
22122 if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
22123 // Invalid module ID.
22124 return;
22125 }
22126
22127 if ( $plugin_id == $this->get_id() ) {
22128 if ( $this->is_premium() ) {
22129 // Already using the premium code version.
22130 return;
22131 }
22132 if ( ! $this->can_use_premium_code() ) {
22133 // Don't have access to the premium code.
22134 return;
22135 }
22136 if ( ! $this->has_release_on_freemius() ) {
22137 // Plugin is a serviceware, no premium code version.
22138 return;
22139 }
22140 } else {
22141 $addon = $this->get_addon( $plugin_id );
22142
22143 if ( ! is_object( $addon ) ) {
22144 // Invalid add-on ID.
22145 return;
22146 }
22147
22148 if ( $this->is_addon_activated( $plugin_id, true ) ) {
22149 // Premium add-on version is already activated.
22150 return;
22151 }
22152 }
22153
22154 $vars = array(
22155 'id' => $this->_module_id,
22156 'target_module_id' => $plugin_id,
22157 'slug' => $this->_slug,
22158 );
22159
22160 fs_require_template( 'auto-installation.php', $vars );
22161 }
22162
22163 #endregion
22164
22165 #--------------------------------------------------------------------------------
22166 #region Tabs Integration
22167 #--------------------------------------------------------------------------------
22168
22169 #region Module's Original Tabs
22170
22171 /**
22172 * Inject a JavaScript logic to capture the theme tabs HTML.
22173 *
22174 * @author Vova Feldman (@svovaf)
22175 * @since 1.2.2.7
22176 */
22177 function _tabs_capture() {
22178 $this->_logger->entrance();
22179
22180 if ( ! $this->is_theme_settings_page() ||
22181 ! $this->is_matching_url( $this->main_menu_url() )
22182 ) {
22183 return;
22184 }
22185
22186 $params = array(
22187 'id' => $this->_module_id,
22188 );
22189
22190 fs_require_once_template( 'tabs-capture-js.php', $params );
22191 }
22192
22193 /**
22194 * Cache theme's tabs HTML for a week. The cache will also be set as expired
22195 * after version and type (free/premium) changes, in addition to the week period.
22196 *
22197 * @author Vova Feldman (@svovaf)
22198 * @since 1.2.2.7
22199 */
22200 function _store_tabs_ajax_action() {
22201 $this->_logger->entrance();
22202
22203 $this->check_ajax_referer( 'store_tabs' );
22204
22205 // Init filesystem if not yet initiated.
22206 WP_Filesystem();
22207
22208 // Get POST body HTML data.
22209 global $wp_filesystem;
22210 $tabs_html = $wp_filesystem->get_contents( "php://input" );
22211
22212 if ( is_string( $tabs_html ) ) {
22213 $tabs_html = trim( $tabs_html );
22214 }
22215
22216 if ( ! is_string( $tabs_html ) || empty( $tabs_html ) ) {
22217 self::shoot_ajax_failure();
22218 }
22219
22220 $this->_cache->set( 'tabs', $tabs_html, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
22221
22222 self::shoot_ajax_success();
22223 }
22224
22225 /**
22226 * Cache theme's settings page custom styles. The cache will also be set as expired
22227 * after version and type (free/premium) changes, in addition to the week period.
22228 *
22229 * @author Vova Feldman (@svovaf)
22230 * @since 1.2.2.7
22231 */
22232 function _store_tabs_styles() {
22233 $this->_logger->entrance();
22234
22235 if ( ! $this->is_theme_settings_page() ||
22236 ! $this->is_matching_url( $this->main_menu_url() )
22237 ) {
22238 return;
22239 }
22240
22241 $wp_styles = wp_styles();
22242
22243 $theme_styles_url = get_template_directory_uri();
22244
22245 $stylesheets = array();
22246 foreach ( $wp_styles->queue as $handler ) {
22247 if ( fs_starts_with( $handler, 'fs_' ) ) {
22248 // Assume that stylesheets that their handler starts with "fs_" belong to the SDK.
22249 continue;
22250 }
22251
22252 /**
22253 * @var _WP_Dependency $stylesheet
22254 */
22255 $stylesheet = $wp_styles->registered[ $handler ];
22256
22257 if ( fs_starts_with( $stylesheet->src, $theme_styles_url ) ) {
22258 $stylesheets[] = $stylesheet->src;
22259 }
22260 }
22261
22262 if ( ! empty( $stylesheets ) ) {
22263 $this->_cache->set( 'tabs_stylesheets', $stylesheets, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
22264 }
22265 }
22266
22267 /**
22268 * Check if module's original settings page has any tabs.
22269 *
22270 * @author Vova Feldman (@svovaf)
22271 * @since 1.2.2.7
22272 *
22273 * @return bool
22274 */
22275 private function has_tabs() {
22276 return $this->_cache->has( 'tabs' );
22277 }
22278
22279 /**
22280 * Get module's settings page HTML content, starting
22281 * from the beginning of the <div class="wrap"> element,
22282 * until the tabs HTML (including).
22283 *
22284 * @author Vova Feldman (@svovaf)
22285 * @since 1.2.2.7
22286 *
22287 * @return string
22288 */
22289 private function get_tabs_html() {
22290 $this->_logger->entrance();
22291
22292 return $this->_cache->get( 'tabs' );
22293 }
22294
22295 /**
22296 * Check if page should include tabs.
22297 *
22298 * @author Vova Feldman (@svovaf)
22299 * @since 1.2.2.7
22300 *
22301 * @return bool
22302 */
22303 private function should_page_include_tabs() {
22304 if ( ! $this->has_settings_menu() ) {
22305 // Don't add tabs if no settings at all.
22306 return false;
22307 }
22308
22309 if ( ! $this->is_theme() ) {
22310 // Only add tabs to themes for now.
22311 return false;
22312 }
22313
22314 if ( ! $this->has_paid_plan() && ! $this->has_addons() ) {
22315 // Only add tabs to monetizing themes.
22316 return false;
22317 }
22318
22319 if ( ! $this->is_theme_settings_page() ) {
22320 // Only add tabs if browsing one of the theme's setting pages.
22321 return false;
22322 }
22323
22324 if ( $this->is_admin_page( 'pricing' ) && fs_request_get_bool( 'checkout' ) ) {
22325 // Don't add tabs on checkout page, we want to reduce distractions
22326 // as much as possible.
22327 return false;
22328 }
22329
22330 return true;
22331 }
22332
22333 /**
22334 * Add the tabs HTML before the setting's page content and
22335 * enqueue any required stylesheets.
22336 *
22337 * @author Vova Feldman (@svovaf)
22338 * @since 1.2.2.7
22339 *
22340 * @return bool If tabs were included.
22341 */
22342 function _add_tabs_before_content() {
22343 $this->_logger->entrance();
22344
22345 if ( ! $this->should_page_include_tabs() ) {
22346 return false;
22347 }
22348
22349 /**
22350 * Enqueue the original stylesheets that are included in the
22351 * theme settings page. That way, if the theme settings has
22352 * some custom _styled_ content above the tabs UI, this
22353 * will make sure that the styling is preserved.
22354 */
22355 $stylesheets = $this->_cache->get( 'tabs_stylesheets', array() );
22356 if ( is_array( $stylesheets ) ) {
22357 for ( $i = 0, $len = count( $stylesheets ); $i < $len; $i ++ ) {
22358 wp_enqueue_style( "fs_{$this->_module_id}_tabs_{$i}", $stylesheets[ $i ] );
22359 }
22360 }
22361
22362 // Cut closing </div> tag.
22363 echo substr( trim( $this->get_tabs_html() ), 0, - 6 );
22364
22365 return true;
22366 }
22367
22368 /**
22369 * Add the tabs closing HTML after the setting's page content.
22370 *
22371 * @author Vova Feldman (@svovaf)
22372 * @since 1.2.2.7
22373 *
22374 * @return bool If tabs closing HTML was included.
22375 */
22376 function _add_tabs_after_content() {
22377 $this->_logger->entrance();
22378
22379 if ( ! $this->should_page_include_tabs() ) {
22380 return false;
22381 }
22382
22383 echo '</div>';
22384
22385 return true;
22386 }
22387
22388 #endregion
22389
22390 /**
22391 * Add in-page JavaScript to inject the Freemius tabs into
22392 * the module's setting tabs section.
22393 *
22394 * @author Vova Feldman (@svovaf)
22395 * @since 1.2.2.7
22396 */
22397 function _add_freemius_tabs() {
22398 $this->_logger->entrance();
22399
22400 if ( ! $this->should_page_include_tabs() ) {
22401 return;
22402 }
22403
22404 $params = array( 'id' => $this->_module_id );
22405 fs_require_once_template( 'tabs.php', $params );
22406 }
22407
22408 #endregion
22409
22410 #--------------------------------------------------------------------------------
22411 #region Customizer Integration for Themes
22412 #--------------------------------------------------------------------------------
22413
22414 /**
22415 * @author Vova Feldman (@svovaf)
22416 * @since 1.2.2.7
22417 *
22418 * @param WP_Customize_Manager $customizer
22419 */
22420 function _customizer_register( $customizer ) {
22421 $this->_logger->entrance();
22422
22423 if ( $this->is_pricing_page_visible() ) {
22424 require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-upsell-control.php';
22425
22426 $customizer->add_section( 'freemius_upsell', array(
22427 'title' => '&#9733; ' . $this->get_text_inline( 'View paid features', 'view-paid-features' ),
22428 'priority' => 1,
22429 ) );
22430 $customizer->add_setting( 'freemius_upsell', array(
22431 'sanitize_callback' => 'esc_html',
22432 ) );
22433
22434 $customizer->add_control( new FS_Customizer_Upsell_Control( $customizer, 'freemius_upsell', array(
22435 'fs' => $this,
22436 'section' => 'freemius_upsell',
22437 'priority' => 100,
22438 ) ) );
22439 }
22440
22441 if ( $this->is_page_visible( 'contact' ) || $this->is_page_visible( 'support' ) ) {
22442 require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-support-section.php';
22443
22444 // Main Documentation Link In Customizer Root.
22445 $customizer->add_section( new FS_Customizer_Support_Section( $customizer, 'freemius_support', array(
22446 'fs' => $this,
22447 'priority' => 1000,
22448 ) ) );
22449 }
22450 }
22451
22452 #endregion
22453
22454 /**
22455 * If the theme has a paid version, add some custom
22456 * styling to the theme's premium version (if exists)
22457 * to highlight that it's the premium version of the
22458 * same theme, making it easier for identification
22459 * after the user upgrades and upload it to the site.
22460 *
22461 * @author Vova Feldman (@svovaf)
22462 * @since 1.2.2.7
22463 */
22464 function _style_premium_theme() {
22465 $this->_logger->entrance();
22466
22467 if ( ! self::is_themes_page() ) {
22468 // Only include in the themes page.
22469 return;
22470 }
22471
22472 if ( ! $this->has_paid_plan() ) {
22473 // Only include if has any paid plans.
22474 return;
22475 }
22476
22477 $params = null;
22478 fs_require_once_template( '/js/jquery.content-change.php', $params );
22479
22480 $params = array(
22481 'slug' => $this->_slug,
22482 'id' => $this->_module_id,
22483 );
22484
22485 fs_require_template( '/js/style-premium-theme.php', $params );
22486 }
22487
22488 /**
22489 * This method will return the absolute URL of the module's local icon.
22490 *
22491 * When you are running your plugin or theme on a **localhost** environment, if the icon
22492 * is not found in the local assets folder, try to fetch the icon URL from Freemius. If not set and
22493 * it's a plugin hosted on WordPress.org, try fetching the icon URL from wordpress.org.
22494 * If an icon is found, this method will automatically attempt to download the icon and store it
22495 * in /freemius/assets/img/{slug}.{png|jpg|gif|svg}.
22496 *
22497 * It's important to mention that this method is NOT phoning home since the developer will deploy
22498 * the product with the local icon in the assets folder. The download process just simplifies
22499 * the process for the developer.
22500 *
22501 * @author Vova Feldman (@svovaf)
22502 * @since 2.0.0
22503 *
22504 * @return string
22505 */
22506 function get_local_icon_url() {
22507 global $fs_active_plugins;
22508
22509 /**
22510 * @since 1.1.7.5
22511 */
22512 $local_path = $this->apply_filters( 'plugin_icon', false );
22513
22514 if ( is_string( $local_path ) ) {
22515 $icons = array( $local_path );
22516 } else {
22517 $img_dir = WP_FS__DIR_IMG;
22518
22519 // Locate the main assets folder.
22520 if ( 1 < count( $fs_active_plugins->plugins ) ) {
22521 $plugin_or_theme_img_dir = ( $this->is_plugin() ? WP_PLUGIN_DIR : get_theme_root( get_stylesheet() ) );
22522
22523 foreach ( $fs_active_plugins->plugins as $sdk_path => &$data ) {
22524 if ( $data->plugin_path == $this->get_plugin_basename() ) {
22525 $img_dir = $plugin_or_theme_img_dir
22526 . '/'
22527 /**
22528 * The basename will be `themes` or the basename of a custom themes directory.
22529 *
22530 * @author Leo Fajardo (@leorw)
22531 * @since 2.2.3
22532 */
22533 . str_replace( '../' . basename( $plugin_or_theme_img_dir ) . '/', '', $sdk_path )
22534 . '/assets/img';
22535
22536 break;
22537 }
22538 }
22539 }
22540
22541 // Try to locate the icon in the assets folder.
22542 $icons = glob( fs_normalize_path( $img_dir . "/{$this->_slug}.*" ) );
22543
22544 if ( ! is_array( $icons ) || 0 === count( $icons ) ) {
22545 if ( ! WP_FS__IS_LOCALHOST && $this->is_theme() ) {
22546 $icons = array(
22547 fs_normalize_path( $img_dir . '/theme-icon.png' )
22548 );
22549 } else {
22550 $icon_found = false;
22551 $local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.png" );
22552
22553 if ( ! function_exists( 'get_filesystem_method' ) ) {
22554 require_once ABSPATH . 'wp-admin/includes/file.php';
22555 }
22556
22557 $have_write_permissions = ( 'direct' === get_filesystem_method( array(), fs_normalize_path( $img_dir ) ) );
22558
22559 /**
22560 * IMPORTANT: THIS CODE WILL NEVER RUN AFTER THE PLUGIN IS IN THE REPO.
22561 *
22562 * This code will only be executed once during the testing
22563 * of the plugin in a local environment. The plugin icon file WILL
22564 * already exist in the assets folder when the plugin is deployed to
22565 * the repository.
22566 */
22567 if ( WP_FS__IS_LOCALHOST && $have_write_permissions ) {
22568 // Fetch icon from Freemius.
22569 $icon = $this->fetch_remote_icon_url();
22570
22571 // Fetch icon from WordPress.org.
22572 if ( empty( $icon ) && $this->is_plugin() && $this->is_org_repo_compliant() ) {
22573 if ( ! function_exists( 'plugins_api' ) ) {
22574 require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
22575 }
22576
22577 $plugin_information = plugins_api( 'plugin_information', array(
22578 'slug' => $this->_slug,
22579 'fields' => array(
22580 'sections' => false,
22581 'tags' => false,
22582 'icons' => true
22583 )
22584 ) );
22585
22586 if (
22587 ! is_wp_error( $plugin_information )
22588 && isset( $plugin_information->icons )
22589 && ! empty( $plugin_information->icons )
22590 ) {
22591 /**
22592 * Get the smallest icon.
22593 *
22594 * @author Leo Fajardo (@leorw)
22595 * @since 1.2.2
22596 */
22597 $icon = end( $plugin_information->icons );
22598 }
22599 }
22600
22601 if ( ! empty( $icon ) ) {
22602 if ( 0 !== strpos( $icon, 'http' ) ) {
22603 $icon = 'http:' . $icon;
22604 }
22605
22606 /**
22607 * Get a clean file extension, e.g.: "jpg" and not "jpg?rev=1305765".
22608 *
22609 * @author Leo Fajardo (@leorw)
22610 * @since 1.2.2
22611 */
22612 $ext = pathinfo( strtok( $icon, '?' ), PATHINFO_EXTENSION );
22613
22614 $local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.{$ext}" );
22615
22616 // Try to download the icon.
22617 $icon_found = fs_download_image( $icon, $local_path );
22618 }
22619 }
22620
22621 if ( ! $icon_found ) {
22622 // No icons found, fallback to default icon.
22623 if ( $have_write_permissions ) {
22624 // If have write permissions, copy default icon.
22625 copy( fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" ), $local_path );
22626 } else {
22627 // If doesn't have write permissions, use default icon path.
22628 $local_path = fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" );
22629 }
22630 }
22631
22632 $icons = array( $local_path );
22633 }
22634 }
22635 }
22636
22637 $icon_dir = dirname( $icons[0] );
22638
22639 return fs_img_url( substr( $icons[0], strlen( $icon_dir ) ), $icon_dir );
22640 }
22641
22642 /**
22643 * Fetch module's extended info.
22644 *
22645 * @author Vova Feldman (@svovaf)
22646 * @since 2.0.0
22647 *
22648 * @return object|mixed
22649 */
22650 private function fetch_module_info() {
22651 return $this->get_api_plugin_scope()->get( 'info.json', false, WP_FS__TIME_WEEK_IN_SEC );
22652 }
22653
22654 /**
22655 * Fetch module's remote icon URL.
22656 *
22657 * @author Vova Feldman (@svovaf)
22658 * @since 2.0.0
22659 *
22660 * @return string
22661 */
22662 function fetch_remote_icon_url() {
22663 $info = $this->fetch_module_info();
22664
22665 return ( $this->is_api_result_object( $info, 'icon' ) && is_string( $info->icon ) ) ?
22666 $info->icon :
22667 '';
22668 }
22669
22670 #--------------------------------------------------------------------------------
22671 #region GDPR
22672 #--------------------------------------------------------------------------------
22673
22674 /**
22675 * @author Leo Fajardo (@leorw)
22676 * @since 2.1.0
22677 *
22678 * @return bool
22679 */
22680 function fetch_and_store_current_user_gdpr_anonymously() {
22681 $pong = $this->ping( null, true );
22682
22683 if ( ! $this->get_api_plugin_scope()->is_valid_ping( $pong ) ) {
22684 return false;
22685 } else {
22686 FS_GDPR_Manager::instance()->store_is_required( $pong->is_gdpr_required );
22687
22688 return $pong->is_gdpr_required;
22689 }
22690 }
22691
22692 /**
22693 * @author Leo Fajardo (@leorw)
22694 * @since 2.1.0
22695 *
22696 * @param array $user_plugins
22697 *
22698 * @return string
22699 */
22700 private function get_gdpr_admin_notice_string( $user_plugins ) {
22701 $this->_logger->entrance();
22702
22703 $addons = self::get_all_addons();
22704
22705 foreach ( $user_plugins as $user_plugin ) {
22706 $has_addons = isset( $addons[ $user_plugin->id ] );
22707
22708 if ( WP_FS__MODULE_TYPE_PLUGIN === $user_plugin->type && ! $has_addons ) {
22709 if ( $this->_module_id == $user_plugin->id ) {
22710 $addons = $this->get_addons();
22711 $has_addons = ( ! empty( $addons ) );
22712 } else {
22713 $plugin_api = FS_Api::instance(
22714 $user_plugin->id,
22715 'plugin',
22716 $user_plugin->id,
22717 $user_plugin->public_key,
22718 ! $user_plugin->is_live,
22719 false,
22720 $this->get_sdk_version()
22721 );
22722
22723 $addons_result = $plugin_api->get( '/addons.json?enriched=true', true );
22724
22725 if ( $this->is_api_result_object( $addons_result, 'plugins' ) &&
22726 is_array( $addons_result->plugins ) &&
22727 ! empty( $addons_result->plugins )
22728 ) {
22729 $has_addons = true;
22730 }
22731 }
22732 }
22733
22734 $user_plugin->has_addons = $has_addons;
22735 }
22736
22737 $is_single_parent_product = ( 1 === count( $user_plugins ) );
22738
22739 $multiple_products_text = '';
22740
22741 if ( $is_single_parent_product ) {
22742 $single_parent_product = reset( $user_plugins );
22743
22744 $thank_you = sprintf(
22745 "<span data-plugin-id='%d'>%s</span>",
22746 $single_parent_product->id,
22747 sprintf(
22748 $single_parent_product->has_addons ?
22749 $this->get_text_inline( 'Thank you so much for using %s and its add-ons!', 'thank-you-for-using-product-and-its-addons' ) :
22750 $this->get_text_inline( 'Thank you so much for using %s!', 'thank-you-for-using-product' ),
22751 sprintf('<b><i>%s</i></b>', $single_parent_product->title)
22752 )
22753 );
22754
22755 $already_opted_in = sprintf(
22756 $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' ),
22757 ( WP_FS__MODULE_TYPE_THEME === $single_parent_product->type ) ? WP_FS__MODULE_TYPE_THEME : WP_FS__MODULE_TYPE_PLUGIN
22758 );
22759 } else {
22760 $thank_you = $this->get_text_inline( 'Thank you so much for using our products!', 'thank-you-for-using-products' );
22761 $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' );
22762
22763 $products_and_add_ons = '';
22764 foreach ( $user_plugins as $user_plugin ) {
22765 if ( ! empty( $products_and_add_ons ) ) {
22766 $products_and_add_ons .= ', ';
22767 }
22768
22769 if ( ! $user_plugin->has_addons ) {
22770 $products_and_add_ons .= sprintf(
22771 "<span data-plugin-id='%d'>%s</span>",
22772 $user_plugin->id,
22773 $user_plugin->title
22774 );
22775 } else {
22776 $products_and_add_ons .= sprintf(
22777 "<span data-plugin-id='%d'>%s</span>",
22778 $user_plugin->id,
22779 sprintf(
22780 $this->get_text_inline( '%s and its add-ons', 'product-and-its-addons' ),
22781 $user_plugin->title
22782 )
22783 );
22784 }
22785 }
22786
22787 $multiple_products_text = sprintf(
22788 "<small class='products'><strong>%s:</strong> %s</small>",
22789 $this->get_text_inline( 'Products', 'products' ),
22790 $products_and_add_ons
22791 );
22792 }
22793
22794 $actions = sprintf(
22795 '<ul><li>%s<span class="action-description"> - %s</span></li><li>%s<span class="action-description"> - %s</span></li></ul>',
22796 sprintf('<button class="button button-primary allow-marketing">%s</button>', $this->get_text_inline( 'Yes', 'yes' ) ),
22797 $this->get_text_inline( 'send me security & feature updates, educational content and offers.', 'send-updates' ),
22798 sprintf('<button class="button button-secondary">%s</button>', $this->get_text_inline( 'No', 'no' ) ),
22799 sprintf(
22800 $this->get_text_inline( 'do %sNOT%s send me security & feature updates, educational content and offers.', 'do-not-send-updates' ),
22801 '<span class="underlined">',
22802 '</span>'
22803 )
22804 );
22805
22806 return sprintf(
22807 '%s %s %s',
22808 $thank_you,
22809 $already_opted_in,
22810 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>' ) .
22811 '<br><br>' .
22812 '<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>' .
22813 $actions .
22814 ( $is_single_parent_product ? '' : $multiple_products_text )
22815 );
22816 }
22817
22818 /**
22819 * This method is called for opted-in users to fetch the is_marketing_allowed flag of the user for all the
22820 * plugins and themes they've opted in to.
22821 *
22822 * @author Leo Fajardo (@leorw)
22823 * @since 2.1.0
22824 *
22825 * @param string $user_email
22826 * @param string $license_key
22827 * @param array $plugin_ids
22828 * @param string|null $license_key
22829 *
22830 * @return array|false
22831 */
22832 private function fetch_user_marketing_flag_status_by_plugins( $user_email, $license_key, $plugin_ids ) {
22833 $request = array(
22834 'method' => 'POST',
22835 'body' => array(),
22836 'timeout' => WP_FS__DEBUG_SDK ? 60 : 30,
22837 );
22838
22839 if ( is_string( $user_email ) ) {
22840 $request['body']['email'] = $user_email;
22841 } else {
22842 $request['body']['license_key'] = $license_key;
22843 }
22844
22845 $result = array();
22846
22847 $url = WP_FS__ADDRESS . '/action/service/user_plugin/';
22848 $total_plugin_ids = count( $plugin_ids );
22849
22850 $plugin_ids_count_per_request = 10;
22851 for ( $i = 1; $i <= $total_plugin_ids; $i += $plugin_ids_count_per_request ) {
22852 $plugin_ids_set = array_slice( $plugin_ids, $i - 1, $plugin_ids_count_per_request );
22853
22854 $request['body']['plugin_ids'] = $plugin_ids_set;
22855
22856 $response = self::safe_remote_post(
22857 $url,
22858 $request,
22859 WP_FS__TIME_24_HOURS_IN_SEC,
22860 WP_FS__TIME_12_HOURS_IN_SEC
22861 );
22862
22863 if ( ! is_wp_error( $response ) ) {
22864 $decoded = is_string( $response['body'] ) ?
22865 json_decode( $response['body'] ) :
22866 null;
22867
22868 if (
22869 !is_object($decoded) ||
22870 !isset($decoded->success) ||
22871 true !== $decoded->success ||
22872 !isset( $decoded->data ) ||
22873 !is_array( $decoded->data )
22874 ) {
22875 return false;
22876 }
22877
22878 $result = array_merge( $result, $decoded->data );
22879 }
22880 }
22881
22882 return $result;
22883 }
22884
22885 /**
22886 * @author Leo Fajardo (@leorw)
22887 * @since 2.1.0
22888 */
22889 function _maybe_show_gdpr_admin_notice() {
22890 if ( ! $this->is_user_in_admin() ) {
22891 return;
22892 }
22893
22894 if ( ! $this->should_handle_gdpr_admin_notice() ) {
22895 return;
22896 }
22897
22898 if ( ! $this->is_user_admin() ) {
22899 return;
22900 }
22901
22902 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
22903
22904 $lock = FS_User_Lock::instance();
22905
22906 /**
22907 * Try to acquire a 60-sec lock based on the WP user and thread/process ID.
22908 */
22909 if ( ! $lock->try_lock( 60 ) ) {
22910 return;
22911 }
22912
22913 /**
22914 * @var $current_wp_user WP_User
22915 */
22916 $current_wp_user = self::_get_current_wp_user();
22917
22918 /**
22919 * @var FS_User $current_fs_user
22920 */
22921 $current_fs_user = Freemius::_get_user_by_email( $current_wp_user->user_email );
22922
22923 $ten_years_in_sec = 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC;
22924
22925 if ( ! is_object( $current_fs_user ) ) {
22926 // 10-year lock.
22927 $lock->lock( $ten_years_in_sec );
22928
22929 return;
22930 }
22931
22932 $gdpr = FS_GDPR_Manager::instance();
22933
22934 if ( $gdpr->is_opt_in_notice_shown() ) {
22935 // 30-day lock.
22936 $lock->lock( 30 * WP_FS__TIME_24_HOURS_IN_SEC );
22937
22938 return;
22939 }
22940
22941 if ( ! $gdpr->should_show_opt_in_notice() ) {
22942 // 10-year lock.
22943 $lock->lock( $ten_years_in_sec );
22944
22945 return;
22946 }
22947
22948 $last_time_notice_shown = $gdpr->last_time_notice_was_shown();
22949 $was_notice_shown_before = ( false !== $last_time_notice_shown );
22950
22951 if ( $was_notice_shown_before &&
22952 30 * WP_FS__TIME_24_HOURS_IN_SEC > time() - $last_time_notice_shown
22953 ) {
22954 // If the notice was shown before, show it again after 30 days from the last time it was shown.
22955 return;
22956 }
22957
22958 /**
22959 * Find all plugin IDs that were installed by the current admin.
22960 */
22961 $plugin_ids_map = self::get_user_opted_in_module_ids_map( $current_fs_user->id );
22962
22963 if ( empty( $plugin_ids_map )) {
22964 $lock->lock( $ten_years_in_sec );
22965
22966 return;
22967 }
22968
22969 $user_plugins = $this->fetch_user_marketing_flag_status_by_plugins(
22970 $current_fs_user->email,
22971 null,
22972 array_keys( $plugin_ids_map )
22973 );
22974
22975 if ( empty( $user_plugins ) ) {
22976 $lock->lock(
22977 is_array($user_plugins) ?
22978 $ten_years_in_sec :
22979 // Lock for 24-hours on errors.
22980 WP_FS__TIME_24_HOURS_IN_SEC
22981 );
22982
22983 return;
22984 }
22985
22986 $has_unset_marketing_optin = false;
22987
22988 foreach ( $user_plugins as $user_plugin ) {
22989 if ( true == $user_plugin->is_marketing_allowed ) {
22990 unset( $plugin_ids_map[ $user_plugin->plugin_id ] );
22991 }
22992
22993 if ( ! $has_unset_marketing_optin && is_null( $user_plugin->is_marketing_allowed ) ) {
22994 $has_unset_marketing_optin = true;
22995 }
22996 }
22997
22998 if ( empty( $plugin_ids_map ) ||
22999 ( $was_notice_shown_before && ! $has_unset_marketing_optin )
23000 ) {
23001 $lock->lock( $ten_years_in_sec );
23002
23003 return;
23004 }
23005
23006 $modules = array_merge(
23007 array_values( self::$_accounts->get_option( 'plugins', array() ) ),
23008 array_values( self::$_accounts->get_option( 'themes', array() ) )
23009 );
23010
23011 foreach ( $modules as $module ) {
23012 if ( ! FS_Plugin::is_valid_id( $module->parent_plugin_id ) && isset( $plugin_ids_map[ $module->id ] ) ) {
23013 $plugin_ids_map[ $module->id ] = $module;
23014 }
23015 }
23016
23017 $plugin_title = null;
23018 if ( 1 === count( $plugin_ids_map ) ) {
23019 $module = reset( $plugin_ids_map );
23020 $plugin_title = $module->title;
23021 }
23022
23023 $gdpr->add_opt_in_sticky_notice(
23024 $this->get_gdpr_admin_notice_string( $plugin_ids_map ),
23025 $plugin_title
23026 );
23027
23028 $this->add_gdpr_optin_ajax_handler_and_style();
23029
23030 $gdpr->notice_was_just_shown();
23031
23032 // 30-day lock.
23033 $lock->lock( 30 * WP_FS__TIME_24_HOURS_IN_SEC );
23034 }
23035
23036 /**
23037 * Prevents the GDPR opt-in admin notice from being added if the user has already chosen to allow or not allow
23038 * marketing.
23039 *
23040 * @author Leo Fajardo (@leorw)
23041 * @since 2.1.0
23042 */
23043 private function disable_opt_in_notice_and_lock_user() {
23044 FS_GDPR_Manager::instance()->disable_opt_in_notice();
23045
23046 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
23047
23048 // 10-year lock.
23049 FS_User_Lock::instance()->lock( 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC );
23050 }
23051
23052 /**
23053 * @author Leo Fajardo (@leorw)
23054 * @since 2.1.0
23055 */
23056 function _add_gdpr_optin_js() {
23057 $vars = array( 'id' => $this->_module_id );
23058
23059 fs_require_once_template( 'gdpr-optin-js.php', $vars );
23060 }
23061
23062 /**
23063 * @author Leo Fajardo (@leorw)
23064 * @since 2.1.0
23065 */
23066 function enqueue_gdpr_optin_notice_style() {
23067 fs_enqueue_local_style( 'fs_gdpr_optin_notice', '/admin/gdpr-optin-notice.css' );
23068 }
23069
23070 /**
23071 * @author Leo Fajardo (@leorw)
23072 * @since 2.1.0
23073 */
23074 function _maybe_add_gdpr_optin_ajax_handler() {
23075 $this->add_ajax_action( 'fetch_is_marketing_required_flag_value', array( &$this, '_fetch_is_marketing_required_flag_value_ajax_action' ) );
23076
23077 if ( FS_GDPR_Manager::instance()->is_opt_in_notice_shown() ) {
23078 $this->add_gdpr_optin_ajax_handler_and_style();
23079 }
23080 }
23081
23082 /**
23083 * @author Leo Fajardo (@leorw)
23084 * @since 2.1.0
23085 */
23086 function _fetch_is_marketing_required_flag_value_ajax_action() {
23087 $this->_logger->entrance();
23088
23089 $this->check_ajax_referer( 'fetch_is_marketing_required_flag_value' );
23090
23091 $license_key = fs_request_get( 'license_key' );
23092
23093 if ( empty($license_key) ) {
23094 self::shoot_ajax_failure( $this->get_text_inline( 'License key is empty.', 'empty-license-key' ) );
23095 }
23096
23097 $user_plugins = $this->fetch_user_marketing_flag_status_by_plugins(
23098 null,
23099 $license_key,
23100 array( $this->_module_id )
23101 );
23102
23103 if ( ! is_array( $user_plugins ) ||
23104 empty($user_plugins) ||
23105 !isset($user_plugins[0]->plugin_id) ||
23106 $user_plugins[0]->plugin_id != $this->_module_id
23107 ) {
23108 /**
23109 * If faced an error or if the module ID do not match to the current module, ask for GDPR opt-in.
23110 *
23111 * @author Vova Feldman (@svovaf)
23112 */
23113 self::shoot_ajax_success( array( 'is_marketing_allowed' => null ) );
23114 }
23115
23116 self::shoot_ajax_success( array( 'is_marketing_allowed' => $user_plugins[0]->is_marketing_allowed ) );
23117 }
23118
23119 /**
23120 * @author Leo Fajardo (@leorw)
23121 * @since 2.1.0
23122 */
23123 private function add_gdpr_optin_ajax_handler_and_style() {
23124 // Add GDPR action AJAX callback.
23125 $this->add_ajax_action( 'gdpr_optin_action', array( &$this, '_gdpr_optin_ajax_action' ) );
23126
23127 add_action( 'admin_footer', array( &$this, '_add_gdpr_optin_js' ) );
23128 add_action( 'admin_enqueue_scripts', array( &$this, 'enqueue_gdpr_optin_notice_style' ) );
23129 }
23130
23131 /**
23132 * @author Leo Fajardo (@leorw)
23133 * @since 2.1.0
23134 */
23135 function _gdpr_optin_ajax_action() {
23136 $this->_logger->entrance();
23137
23138 $this->check_ajax_referer( 'gdpr_optin_action' );
23139
23140 if ( ! fs_request_has( 'is_marketing_allowed' ) || ! fs_request_has( 'plugin_ids' ) ) {
23141 self::shoot_ajax_failure();
23142 }
23143
23144 $current_wp_user = self::_get_current_wp_user();
23145
23146 $plugin_ids = fs_request_get( 'plugin_ids', array() );
23147 if ( ! is_array( $plugin_ids ) || empty( $plugin_ids ) ) {
23148 self::shoot_ajax_failure();
23149 }
23150
23151 $modules = array_merge(
23152 array_values( self::$_accounts->get_option( 'plugins', array() ) ),
23153 array_values( self::$_accounts->get_option( 'themes', array() ) )
23154 );
23155
23156 foreach ( $modules as $key => $module ) {
23157 if ( ! in_array( $module->id, $plugin_ids ) ) {
23158 unset( $modules[ $key ] );
23159 }
23160 }
23161
23162 if ( empty( $modules ) ) {
23163 self::shoot_ajax_failure();
23164 }
23165
23166 $user_api = $this->get_api_user_scope_by_user( Freemius::_get_user_by_email( $current_wp_user->user_email ) );
23167
23168 foreach ( $modules as $module ) {
23169 $user_api->call( "?plugin_id={$module->id}", 'put', array(
23170 'is_marketing_allowed' => ( true == fs_request_get_bool( 'is_marketing_allowed' ) )
23171 ) );
23172 }
23173
23174 FS_GDPR_Manager::instance()->remove_opt_in_notice();
23175
23176 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
23177
23178 // 10-year lock.
23179 FS_User_Lock::instance()->lock( 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC );
23180
23181 self::shoot_ajax_success();
23182 }
23183
23184 /**
23185 * 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.
23186 *
23187 * @author Vova Feldman (@svovaf)
23188 * @since 2.1.0
23189 *
23190 * @return bool
23191 */
23192 private function should_handle_gdpr_admin_notice() {
23193 return $this->apply_filters(
23194 'handle_gdpr_admin_notice',
23195 // Default to false.
23196 false
23197 );
23198 }
23199
23200 #endregion
23201
23202 #----------------------------------------------------------------------------------
23203 #region Marketing
23204 #----------------------------------------------------------------------------------
23205
23206 /**
23207 * Check if current user purchased any other plugins before.
23208 *
23209 * @author Vova Feldman (@svovaf)
23210 * @since 1.0.9
23211 *
23212 * @return bool
23213 */
23214 function has_purchased_before() {
23215 // TODO: Implement has_purchased_before() method.
23216 throw new Exception( 'not implemented' );
23217 }
23218
23219 /**
23220 * Check if current user classified as an agency.
23221 *
23222 * @author Vova Feldman (@svovaf)
23223 * @since 1.0.9
23224 *
23225 * @return bool
23226 */
23227 function is_agency() {
23228 // TODO: Implement is_agency() method.
23229 throw new Exception( 'not implemented' );
23230 }
23231
23232 /**
23233 * Check if current user classified as a developer.
23234 *
23235 * @author Vova Feldman (@svovaf)
23236 * @since 1.0.9
23237 *
23238 * @return bool
23239 */
23240 function is_developer() {
23241 // TODO: Implement is_developer() method.
23242 throw new Exception( 'not implemented' );
23243 }
23244
23245 /**
23246 * Check if current user classified as a business.
23247 *
23248 * @author Vova Feldman (@svovaf)
23249 * @since 1.0.9
23250 *
23251 * @return bool
23252 */
23253 function is_business() {
23254 // TODO: Implement is_business() method.
23255 throw new Exception( 'not implemented' );
23256 }
23257
23258 #endregion
23259
23260 #----------------------------------------------------------------------------------
23261 #region Helper
23262 #----------------------------------------------------------------------------------
23263
23264 /**
23265 * If running with a secret key, assume it's the developer and show pending plans as well.
23266 *
23267 * @author Vova Feldman (@svovaf)
23268 * @since 2.1.2
23269 *
23270 * @param string $path
23271 *
23272 * @return string
23273 */
23274 function add_show_pending( $path ) {
23275 if ( ! $this->has_secret_key() ) {
23276 return $path;
23277 }
23278
23279 return $path . ( false !== strpos( $path, '?' ) ? '&' : '?' ) . 'show_pending=true';
23280 }
23281
23282 #endregion
23283 }
23284