PluginProbe
Tiered Pricing Table for WooCommerce / 2.2.2
Tiered Pricing Table for WooCommerce v2.2.2
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.2, at freemius/includes/class-freemius.php

21,795 lines 814.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 ( is_plugin_active_for_network( $this->_plugin_basename ) ||
403 // Plugin network level activation or uninstall.
404 is_plugin_inactive( $this->_plugin_basename ) )
405 );
406
407 $this->_storage->set_network_active(
408 $this->_is_network_active,
409 $this->is_delegated_connection()
410 );
411
412 #region Migration
413
414 if ( is_multisite() ) {
415 /**
416 * If the install_timestamp exists on the site level but doesn't exist on the
417 * network level storage, it means that we need to process the storage with migration.
418 *
419 * 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.
420 *
421 * @author Vova Feldman (@svovaf)
422 * @since 2.0.0
423 */
424 if ( false === $this->_storage->get( 'install_timestamp', false, true ) &&
425 false !== $this->_storage->get( 'install_timestamp', false, false )
426 ) {
427 // Initiate storage migration.
428 $this->_storage->migrate_to_network();
429
430 // Migrate module cache to network level storage.
431 $this->_cache->migrate_to_network();
432 }
433 }
434
435 #endregion
436
437 $base_name_split = explode( '/', $this->_plugin_basename );
438 $this->_plugin_dir_name = $base_name_split[0];
439
440 if ( $this->_logger->is_on() ) {
441 $this->_logger->info( 'plugin_main_file_path = ' . $this->_plugin_main_file_path );
442 $this->_logger->info( 'plugin_dir_path = ' . $this->_plugin_dir_path );
443 $this->_logger->info( 'plugin_basename = ' . $this->_plugin_basename );
444 $this->_logger->info( 'free_plugin_basename = ' . $this->_free_plugin_basename );
445 $this->_logger->info( 'plugin_dir_name = ' . $this->_plugin_dir_name );
446 }
447
448 // Remember link between file to slug.
449 $this->store_file_slug_map();
450
451 // Store plugin's initial install timestamp.
452 if ( ! isset( $this->_storage->install_timestamp ) ) {
453 $this->_storage->install_timestamp = WP_FS__SCRIPT_START_TIME;
454 }
455
456 if ( ! is_object( $this->_plugin ) ) {
457 $this->_plugin = FS_Plugin_Manager::instance( $this->_module_id )->get();
458 }
459
460 $this->_admin_notices = FS_Admin_Notices::instance(
461 $this->_slug . ( $this->is_theme() ? ':theme' : '' ),
462 /**
463 * Ensure that the admin notice will always have a title by using the stored plugin title if available and
464 * retrieving the title via the "get_plugin_name" method if there is no stored plugin title available.
465 *
466 * @author Leo Fajardo (@leorw)
467 * @since 1.2.2
468 */
469 ( is_object( $this->_plugin ) ? $this->_plugin->title : $this->get_plugin_name() ),
470 $this->get_unique_affix()
471 );
472
473 if ( 'true' === fs_request_get( 'fs_clear_api_cache' ) ||
474 'true' === fs_request_is_action( 'restart_freemius' )
475 ) {
476 FS_Api::clear_cache();
477 $this->_cache->clear();
478 }
479
480 $this->_register_hooks();
481
482 /**
483 * Starting from version 2.0.0, `FS_Site` entities no longer have the `plan` property and have `plan_id`
484 * instead. This should be called before calling `_load_account()`, otherwise, `$this->_site` will not be
485 * loaded in `_load_account` for versions of SDK starting from 2.0.0.
486 *
487 * @author Leo Fajardo (@leorw)
488 */
489 self::migrate_install_plan_to_plan_id( $this->_storage );
490
491 $this->_load_account();
492
493 $this->_version_updates_handler();
494 }
495
496 /**
497 * Checks whether this module has a settings menu.
498 *
499 * @author Leo Fajardo (@leorw)
500 * @since 1.2.2
501 *
502 * @return bool
503 */
504 function has_settings_menu() {
505 return ( $this->_is_network_active && fs_is_network_admin() ) ?
506 $this->_menu->has_network_menu() :
507 $this->_menu->has_menu();
508 }
509
510 /**
511 * Check if the context module is free wp.org theme.
512 *
513 * This method is helpful because:
514 * 1. wp.org themes are limited to a single submenu item,
515 * and sub-submenu items are most likely not allowed (never verified).
516 * 2. wp.org themes are not allowed to redirect the user
517 * after the theme activation, therefore, the agreed UX
518 * is showing the opt-in as a modal dialog box after
519 * activation (approved by @otto42, @emiluzelac, @greenshady, @grapplerulrich).
520 *
521 * @author Vova Feldman (@svovaf)
522 * @since 1.2.2.7
523 *
524 * @return bool
525 */
526 function is_free_wp_org_theme() {
527 return (
528 $this->is_theme() &&
529 $this->is_org_repo_compliant() &&
530 ! $this->is_premium()
531 );
532 }
533
534 /**
535 * Checks whether this a submenu item is visible.
536 *
537 * @author Vova Feldman (@svovaf)
538 * @since 1.2.2.6
539 * @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.
540 *
541 * @param string $slug
542 * @param bool $ignore_free_wp_org_theme_context This is used to decide if the associated tab should be shown
543 * or hidden.
544 *
545 * @return bool
546 */
547 function is_submenu_item_visible( $slug, $ignore_free_wp_org_theme_context = false ) {
548 if ( $this->is_admin_page( $slug ) ) {
549 /**
550 * It is the current context page, so show the submenu item
551 * so the user will have the right context page, even if it
552 * was set to hidden.
553 */
554 return true;
555 }
556
557 if ( ! $this->has_settings_menu() ) {
558 // No menu settings at all.
559 return false;
560 }
561
562 if ( ! $ignore_free_wp_org_theme_context && $this->is_free_wp_org_theme() ) {
563 /**
564 * wp.org themes are limited to a single submenu item, and
565 * sub-submenu items are most likely not allowed (never verified).
566 */
567 return false;
568 }
569
570 return $this->_menu->is_submenu_item_visible( $slug );
571 }
572
573 /**
574 * Check if a Freemius page should be accessible via the UI.
575 *
576 * @author Vova Feldman (@svovaf)
577 * @since 1.2.2.7
578 *
579 * @param string $slug
580 *
581 * @return bool
582 */
583 function is_page_visible( $slug ) {
584 if ( $this->is_admin_page( $slug ) ) {
585 return true;
586 }
587
588 return $this->_menu->is_submenu_item_visible( $slug, true, true );
589 }
590
591 /**
592 * @author Vova Feldman (@svovaf)
593 * @since 1.0.9
594 */
595 private function _version_updates_handler() {
596 if ( ! isset( $this->_storage->sdk_version ) || $this->_storage->sdk_version != $this->version ) {
597 // Freemius version upgrade mode.
598 $this->_storage->sdk_last_version = $this->_storage->sdk_version;
599 $this->_storage->sdk_version = $this->version;
600
601 if ( empty( $this->_storage->sdk_last_version ) ||
602 version_compare( $this->_storage->sdk_last_version, $this->version, '<' )
603 ) {
604 $this->_storage->sdk_upgrade_mode = true;
605 $this->_storage->sdk_downgrade_mode = false;
606 } else {
607 $this->_storage->sdk_downgrade_mode = true;
608 $this->_storage->sdk_upgrade_mode = false;
609
610 }
611
612 $this->do_action( 'sdk_version_update', $this->_storage->sdk_last_version, $this->version );
613 }
614
615 $plugin_version = $this->get_plugin_version();
616 if ( ! isset( $this->_storage->plugin_version ) || $this->_storage->plugin_version != $plugin_version ) {
617 // Plugin version upgrade mode.
618 $this->_storage->plugin_last_version = $this->_storage->plugin_version;
619 $this->_storage->plugin_version = $plugin_version;
620
621 if ( empty( $this->_storage->plugin_last_version ) ||
622 version_compare( $this->_storage->plugin_last_version, $plugin_version, '<' )
623 ) {
624 $this->_storage->plugin_upgrade_mode = true;
625 $this->_storage->plugin_downgrade_mode = false;
626 } else {
627 $this->_storage->plugin_downgrade_mode = true;
628 $this->_storage->plugin_upgrade_mode = false;
629 }
630
631 if ( ! empty( $this->_storage->plugin_last_version ) ) {
632 // Different version of the plugin was installed before, therefore it's an update.
633 $this->_storage->is_plugin_new_install = false;
634 }
635
636 $this->do_action( 'plugin_version_update', $this->_storage->plugin_last_version, $plugin_version );
637 }
638 }
639
640 #--------------------------------------------------------------------------------
641 #region Data Migration on SDK Update
642 #--------------------------------------------------------------------------------
643
644 /**
645 * @author Vova Feldman (@svovaf)
646 * @since 1.1.5
647 *
648 * @param string $sdk_prev_version
649 * @param string $sdk_version
650 */
651 function _sdk_version_update( $sdk_prev_version, $sdk_version ) {
652 /**
653 * @since 1.1.7.3 Fixed unwanted connectivity test cleanup.
654 */
655 if ( empty( $sdk_prev_version ) ) {
656 return;
657 }
658
659 if ( version_compare( $sdk_prev_version, '2.1.0', '<' ) &&
660 version_compare( $sdk_version, '2.1.0', '>=' )
661 ) {
662 $this->_storage->handle_gdpr_admin_notice = true;
663 }
664
665 if ( version_compare( $sdk_prev_version, '2.0.0', '<' ) &&
666 version_compare( $sdk_version, '2.0.0', '>=' )
667 ) {
668 $this->migrate_to_subscriptions_collection();
669
670 $this->consolidate_licenses();
671
672 // Clear trial_plan since it's now loaded from the plans collection when needed.
673 $this->_storage->remove( 'trial_plan', true, false );
674 }
675
676 if ( version_compare( $sdk_prev_version, '1.2.3', '<' ) &&
677 version_compare( $sdk_version, '1.2.3', '>=' )
678 ) {
679 /**
680 * Starting from version 1.2.3, paths are stored as relative instead of absolute and some of them can be
681 * invalid.
682 *
683 * @author Leo Fajardo (@leorw)
684 */
685 $this->remove_invalid_paths();
686 }
687
688 if ( version_compare( $sdk_prev_version, '1.1.5', '<' ) &&
689 version_compare( $sdk_version, '1.1.5', '>=' )
690 ) {
691 // On version 1.1.5 merged connectivity and is_on data.
692 if ( isset( $this->_storage->connectivity_test ) ) {
693 if ( ! isset( $this->_storage->is_on ) ) {
694 unset( $this->_storage->connectivity_test );
695 } else {
696 $connectivity_data = $this->_storage->connectivity_test;
697 $connectivity_data['is_active'] = $this->_storage->is_on['is_active'];
698 $connectivity_data['timestamp'] = $this->_storage->is_on['timestamp'];
699
700 // Override.
701 $this->_storage->connectivity_test = $connectivity_data;
702
703 // Remove previous structure.
704 unset( $this->_storage->is_on );
705 }
706
707 }
708 }
709
710 if (
711 version_compare( $sdk_prev_version, '2.2.1', '<' ) &&
712 version_compare( $sdk_version, '2.2.1', '>=' )
713 ) {
714 /**
715 * Clear the file cache without storing the previous path since it could be a wrong path. For example,
716 * 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
717 * as the parent plugin's when the add-on was auto-installed since the relevant method names were not
718 * skipped in the logic that determines the right path in the `get_caller_main_file_and_type` method
719 * (e.g. `try_activate_plugin`). Since it was an auto-installation, the caller was the parent plugin
720 * and so its path was used. In case the stored path is wrong, clearing the cache will resolve issues
721 * related to data mix-up between plugins (e.g. titles and versions of an add-on and its parent plugin).
722 *
723 * @author Leo Fajardo (@leorw)
724 * @since 2.2.1
725 */
726 $this->clear_module_main_file_cache( false );
727 }
728 }
729
730 /**
731 * @author Leo Fajardo (@leorw)
732 * @since 2.0.0
733 *
734 * @param \FS_Storage $storage
735 * @param bool|int|null $blog_id
736 */
737 private static function migrate_install_plan_to_plan_id( FS_Storage $storage, $blog_id = null ) {
738 if ( empty( $storage->sdk_version ) ) {
739 // New installation of the plugin, no need to upgrade.
740 return;
741 }
742
743 if ( ! version_compare( $storage->sdk_version, '2.0.0', '<' ) ) {
744 // Previous version is >= 2.0.0, so no need to migrate.
745 return;
746 }
747
748 // Alias.
749 $module_type = $storage->get_module_type();
750 $module_slug = $storage->get_module_slug();
751
752 $installs = self::get_all_sites( $module_type, $blog_id );
753 $install = isset( $installs[ $module_slug ] ) ? $installs[ $module_slug ] : null;
754
755 if ( ! is_object( $install ) ) {
756 return;
757 }
758
759 if ( isset( $install->plan ) && is_object( $install->plan ) ) {
760 if ( isset( $install->plan->id ) && ! empty( $install->plan->id ) ) {
761 $install->plan_id = self::_decrypt( $install->plan->id );
762 }
763
764 unset( $install->plan );
765
766 $installs[ $module_slug ] = clone $install;
767
768 self::set_account_option_by_module(
769 $module_type,
770 'sites',
771 $installs,
772 true,
773 $blog_id
774 );
775 }
776 }
777
778 /**
779 * @author Leo Fajardo (@leorw)
780 * @since 2.0.0
781 */
782 private function migrate_to_subscriptions_collection() {
783 if ( ! is_object( $this->_site ) ) {
784 return;
785 }
786
787 if ( isset( $this->_storage->subscription ) && is_object( $this->_storage->subscription ) ) {
788 $this->_storage->subscriptions = array( $this->_storage->subscription );
789 }
790 }
791
792 /**
793 * @author Leo Fajardo (@leorw)
794 * @since 2.0.0
795 */
796 private function consolidate_licenses() {
797 $plugin_licenses = self::get_account_option( 'licenses', WP_FS__MODULE_TYPE_PLUGIN );
798 if ( isset( $plugin_licenses[ $this->_slug ] ) ) {
799 $plugin_licenses = $plugin_licenses[ $this->_slug ];
800 } else {
801 $plugin_licenses = array();
802 }
803
804 $theme_licenses = self::get_account_option( 'licenses', WP_FS__MODULE_TYPE_THEME );
805 if ( isset( $theme_licenses[ $this->_slug ] ) ) {
806 $theme_licenses = $theme_licenses[ $this->_slug ];
807 } else {
808 $theme_licenses = array();
809 }
810
811 if ( empty( $plugin_licenses ) && empty( $theme_licenses ) ) {
812 return;
813 }
814
815 $all_licenses = array();
816 $user_id_license_ids_map = array();
817
818 foreach ( $plugin_licenses as $user_id => $user_licenses ) {
819 if ( is_array( $user_licenses ) ) {
820 if ( ! isset( $user_license_ids[ $user_id ] ) ) {
821 $user_id_license_ids_map[ $user_id ] = array();
822 }
823
824 foreach ( $user_licenses as $user_license ) {
825 $all_licenses[] = $user_license;
826 $user_id_license_ids_map[ $user_id ][] = $user_license->id;
827 }
828 }
829 }
830
831 foreach ( $theme_licenses as $user_id => $user_licenses ) {
832 if ( is_array( $user_licenses ) ) {
833 if ( ! isset( $user_license_ids[ $user_id ] ) ) {
834 $user_id_license_ids_map[ $user_id ] = array();
835 }
836
837 foreach ( $user_licenses as $user_license ) {
838 $all_licenses[] = $user_license;
839 $user_id_license_ids_map[ $user_id ][] = $user_license->id;
840 }
841 }
842 }
843
844 self::store_user_id_license_ids_map(
845 $user_id_license_ids_map,
846 $this->_module_id
847 );
848
849 $this->_store_licenses( true, $this->_module_id, $all_licenses );
850 }
851
852 /**
853 * Remove invalid paths.
854 *
855 * @author Leo Fajardo (@leorw)
856 * @since 1.2.3
857 */
858 private function remove_invalid_paths() {
859 // Remove invalid path that is still associated with the current slug if there's any.
860 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
861 foreach ( $file_slug_map as $plugin_basename => $slug ) {
862 if ( $slug === $this->_slug &&
863 $plugin_basename !== $this->_plugin_basename &&
864 ! file_exists( $this->get_absolute_path( $plugin_basename ) )
865 ) {
866 unset( $file_slug_map[ $plugin_basename ] );
867 self::$_accounts->set_option( 'file_slug_map', $file_slug_map, true );
868
869 break;
870 }
871 }
872 }
873
874 /**
875 * @author Vova Feldman (@svovaf)
876 * @since 1.2.2.7
877 *
878 * @param string $plugin_prev_version
879 * @param string $plugin_version
880 */
881 function _after_version_update( $plugin_prev_version, $plugin_version ) {
882 if ( $this->is_theme() ) {
883 // Expire the cache of the previous tabs since the theme may
884 // have setting updates.
885 $this->_cache->expire( 'tabs' );
886 $this->_cache->expire( 'tabs_stylesheets' );
887 }
888 }
889
890 /**
891 * A special migration logic for the $_accounts, executed for all the plugins in the system:
892 * - Moves some data to the network level storage.
893 * - If the plugin's connection was skipped for all sites, set the plugin as if it was network skipped.
894 * - If the plugin's connection was ignored for all sites, don't do anything in terms of the network connection.
895 * - 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.
896 * - 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.
897 * - 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.
898 *
899 * @author Vova Feldman (@svovaf)
900 * @since 2.0.0
901 */
902 private static function migrate_accounts_to_network() {
903 $sites = self::get_sites();
904 $sites_count = count( $sites );
905 $connection_status = array();
906 $plugin_slugs = array();
907 foreach ( $sites as $site ) {
908 $blog_id = self::get_site_blog_id( $site );
909
910 self::$_accounts->migrate_to_network( $blog_id );
911
912 /**
913 * Build a list of all Freemius powered plugins slugs.
914 */
915 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array(), $blog_id );
916 foreach ( $id_slug_type_path_map as $module_id => $data ) {
917 if ( WP_FS__MODULE_TYPE_PLUGIN === $data['type'] ) {
918 $plugin_slugs[ $data['slug'] ] = true;
919 }
920 }
921
922 $installs = self::get_account_option( 'sites', WP_FS__MODULE_TYPE_PLUGIN, $blog_id );
923
924 if ( is_array( $installs ) ) {
925 foreach ( $installs as $slug => $install ) {
926 if ( ! isset( $connection_status[ $slug ] ) ) {
927 $connection_status[ $slug ] = array();
928 }
929
930 if ( is_object( $install ) &&
931 FS_Site::is_valid_id( $install->id ) &&
932 FS_User::is_valid_id( $install->user_id )
933 ) {
934 $connection_status[ $slug ][ $blog_id ] = $install->user_id;
935 }
936 }
937 }
938 }
939
940 foreach ( $plugin_slugs as $slug => $true ) {
941 if ( ! isset( $connection_status[ $slug ] ) ) {
942 $connection_status[ $slug ] = array();
943 }
944
945 foreach ( $sites as $site ) {
946 $blog_id = self::get_site_blog_id( $site );
947
948 if ( isset( $connection_status[ $slug ][ $blog_id ] ) ) {
949 continue;
950 }
951
952 $storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
953
954 $is_anonymous = $storage->get( 'is_anonymous', null, $blog_id );
955
956 if ( ! is_null( $is_anonymous ) ) {
957 // Since 1.1.3 is_anonymous is an array.
958 if ( is_array( $is_anonymous ) && isset( $is_anonymous['is'] ) ) {
959 $is_anonymous = $is_anonymous['is'];
960 }
961
962 if ( is_bool( $is_anonymous ) && true === $is_anonymous ) {
963 $connection_status[ $slug ][ $blog_id ] = 'skipped';
964 }
965 }
966
967 if ( ! isset( $connection_status[ $slug ][ $blog_id ] ) ) {
968 $connection_status[ $slug ][ $blog_id ] = 'ignored';
969 }
970 }
971 }
972
973 $super_admins = array();
974
975 foreach ( $connection_status as $slug => $blogs_status ) {
976 $skips = 0;
977 $ignores = 0;
978 $connections = 0;
979 $opted_in_users = array();
980 $opted_in_super_admins = array();
981
982 $storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
983
984 foreach ( $blogs_status as $blog_id => $status_or_user_id ) {
985 if ( 'skipped' === $status_or_user_id ) {
986 $skips ++;
987 } else if ( 'ignored' === $status_or_user_id ) {
988 $ignores ++;
989 } else if ( FS_User::is_valid_id( $status_or_user_id ) ) {
990 $connections ++;
991
992 if ( ! isset( $opted_in_users[ $status_or_user_id ] ) ) {
993 $opted_in_users[ $status_or_user_id ] = array();
994 }
995
996 $opted_in_users[ $status_or_user_id ][] = $blog_id;
997
998 if ( isset( $super_admins[ $status_or_user_id ] ) ||
999 self::is_super_admin( $status_or_user_id )
1000 ) {
1001 // Cache super-admin data.
1002 $super_admins[ $status_or_user_id ] = true;
1003
1004 // Remember opted-in super-admins for the plugin.
1005 $opted_in_super_admins[ $status_or_user_id ] = true;
1006 }
1007 }
1008 }
1009
1010 $main_super_admin_user_id = null;
1011 $all_migrated = false;
1012 if ( $sites_count == $skips ) {
1013 // All sites were skipped -> network skip by copying the anonymous mode from any of the sites.
1014 $storage->is_anonymous_ms = $storage->is_anonymous;
1015
1016 $all_migrated = true;
1017 } else if ( $sites_count == $ignores ) {
1018 // Don't do anything, still in activation mode.
1019
1020 $all_migrated = true;
1021 } else if ( 0 < count( $opted_in_super_admins ) ) {
1022 // Find the super-admin with the majority of installs.
1023 $max_installs_by_super_admin = 0;
1024 foreach ( $opted_in_super_admins as $user_id => $true ) {
1025 $installs_count = count( $opted_in_users[ $user_id ] );
1026
1027 if ( $installs_count > $max_installs_by_super_admin ) {
1028 $max_installs_by_super_admin = $installs_count;
1029 $main_super_admin_user_id = $user_id;
1030 }
1031 }
1032
1033 if ( $sites_count == $connections && 1 == count( $opted_in_super_admins ) ) {
1034 // Super-admin opted-in for all sites in the network.
1035 $storage->is_network_connected = true;
1036
1037 $all_migrated = true;
1038 }
1039
1040 // Store network user.
1041 $storage->network_user_id = $main_super_admin_user_id;
1042
1043 $storage->network_install_blog_id = ( $sites_count == $connections ) ?
1044 // Since all sites are opted-in, associating with the main site.
1045 get_current_blog_id() :
1046 // Associating with the 1st found opted-in site.
1047 $opted_in_users[ $main_super_admin_user_id ][0];
1048
1049 /**
1050 * Make sure we migrate the plan ID of the network install, otherwise, if after the migration
1051 * the 1st page that will be loaded is the network level WP Admin and $storage->network_install_blog_id
1052 * is different than the main site of the network, the $this->_site will not be set since the plan_id
1053 * will be empty.
1054 */
1055 $storage->migrate_to_network();
1056 self::migrate_install_plan_to_plan_id( $storage, $storage->network_install_blog_id );
1057 } else {
1058 // At least one opt-in. All the opt-in were created by a non-super-admin.
1059 if ( 0 == $ignores ) {
1060 // All sites were opted-in or skipped, all by non-super-admin. So delegate all.
1061 $storage->store( 'is_delegated_connection', true, true );
1062
1063 $all_migrated = true;
1064 }
1065 }
1066
1067 if ( ! $all_migrated ) {
1068 /**
1069 * Delegate all sites that were:
1070 * 1) Opted-in by a user that is NOT the main-super-admin.
1071 * 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.
1072 */
1073 foreach ( $blogs_status as $blog_id => $status_or_user_id ) {
1074 if ( $status_or_user_id == $main_super_admin_user_id ) {
1075 continue;
1076 }
1077
1078 if ( FS_User::is_valid_id( $status_or_user_id ) ||
1079 ( 'skipped' === $status_or_user_id && is_null( $main_super_admin_user_id ) )
1080 ) {
1081 $storage->store( 'is_delegated_connection', true, $blog_id );
1082 }
1083 }
1084 }
1085
1086
1087 if ( ( $connections + $skips > 0 ) ) {
1088 if ( $ignores > 0 ) {
1089 /**
1090 * If admin already opted-in or skipped in any of the network sites, and also
1091 * have sites which the connection decision was not yet taken, set this plugin
1092 * into network activation mode so the super-admin can choose what to do with
1093 * the rest of the sites.
1094 */
1095 self::set_network_upgrade_mode( $storage );
1096 }
1097 }
1098 }
1099 }
1100
1101 /**
1102 * Set a module into network upgrade mode.
1103 *
1104 * @author Vova Feldman (@svovaf)
1105 * @since 2.0.0
1106 *
1107 * @param \FS_Storage $storage
1108 *
1109 * @return bool
1110 */
1111 private static function set_network_upgrade_mode( FS_Storage $storage ) {
1112 return $storage->is_network_activation = true;
1113 }
1114
1115 /**
1116 * Will return true after upgrading to the SDK with the network level integration,
1117 * when the super-admin involvement is required regarding the rest of the sites.
1118 *
1119 * @author Vova Feldman (@svovaf)
1120 * @since 2.0.0
1121 *
1122 * @return bool
1123 */
1124 function is_network_upgrade_mode() {
1125 return $this->_storage->get( 'is_network_activation' );
1126 }
1127
1128 /**
1129 * Clear flag after the upgrade mode completion.
1130 *
1131 * @author Vova Feldman (@svovaf)
1132 * @since 2.0.0
1133 *
1134 * @return bool True if network activation was on and now completed.
1135 */
1136 private function network_upgrade_mode_completed() {
1137 if ( fs_is_network_admin() && $this->is_network_upgrade_mode() ) {
1138 $this->_storage->remove( 'is_network_activation' );
1139
1140 return true;
1141 }
1142
1143 return false;
1144 }
1145
1146 #endregion
1147
1148 /**
1149 * This action is connected to the 'plugins_loaded' hook and helps to determine
1150 * if this is a new plugin installation or a plugin update.
1151 *
1152 * There are 3 different use-cases:
1153 * 1) New plugin installation right with Freemius:
1154 * 1.1 _activate_plugin_event_hook() will be executed first
1155 * 1.2 Since $this->_storage->is_plugin_new_install is not set,
1156 * and $this->_storage->plugin_last_version is not set,
1157 * $this->_storage->is_plugin_new_install will be set to TRUE.
1158 * 1.3 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install will
1159 * be already set to TRUE.
1160 *
1161 * 2) Plugin update, didn't have Freemius before, and now have the SDK:
1162 * 2.1 _activate_plugin_event_hook() will not be executed, because
1163 * the activation hook do NOT fires on updates since WP 3.1.
1164 * 2.2 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install will
1165 * be empty, therefore, it will be set to FALSE.
1166 *
1167 * 3) Plugin update, had Freemius in prev version as well:
1168 * 3.1 _version_updates_handler() will be executed 1st, since FS was installed
1169 * before, $this->_storage->plugin_last_version will NOT be empty,
1170 * therefore, $this->_storage->is_plugin_new_install will be set to FALSE.
1171 * 3.2 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install is
1172 * already set, therefore, it will not be modified.
1173 *
1174 * Use-case #3 is backward compatible, #3.1 will be executed since 1.0.9.
1175 *
1176 * NOTE:
1177 * The only fallback of this mechanism is if an admin updates a plugin based on use-case #2,
1178 * and then, the next immediate PageView is the plugin's main settings page, it will not
1179 * show the opt-in right away. The reason it will happen is because Freemius execution
1180 * will be turned off till the plugin is fully loaded at least once
1181 * (till $this->_storage->was_plugin_loaded is TRUE).
1182 *
1183 * @author Vova Feldman (@svovaf)
1184 * @since 1.1.9
1185 *
1186 */
1187 function _plugins_loaded() {
1188 // Update flag that plugin was loaded with Freemius at least once.
1189 $this->_storage->was_plugin_loaded = true;
1190
1191 /**
1192 * Bug fix - only set to false when it's a plugin, due to the
1193 * execution sequence of the theme hooks and our methods, if
1194 * this will be set for themes, Freemius will always assume
1195 * it's a theme update.
1196 *
1197 * @author Vova Feldman (@svovaf)
1198 * @since 1.2.2.2
1199 */
1200 if ( $this->is_plugin() &&
1201 ! isset( $this->_storage->is_plugin_new_install )
1202 ) {
1203 $this->_storage->is_plugin_new_install = false;
1204 }
1205 }
1206
1207 /**
1208 * Add special parameter to WP admin AJAX calls so when we
1209 * process AJAX calls we can identify its source properly.
1210 *
1211 * @author Leo Fajardo (@leorw)
1212 * @since 2.0.0
1213 */
1214 static function _enrich_ajax_url() {
1215 $admin_param = is_network_admin() ?
1216 '_fs_network_admin' :
1217 '_fs_blog_admin';
1218 ?>
1219 <script type="text/javascript">
1220 (function ($) {
1221 $(document).ajaxSend(function (event, jqxhr, settings) {
1222 if (settings.url &&
1223 -1 < settings.url.indexOf('admin-ajax.php') &&
1224 ! ( settings.url.indexOf( '<?php echo $admin_param ?>' ) > 0 )
1225 ) {
1226 if (settings.url.indexOf('?') > 0) {
1227 settings.url += '&';
1228 } else {
1229 settings.url += '?';
1230 }
1231
1232 settings.url += '<?php echo $admin_param ?>=true';
1233
1234 }
1235 });
1236 })(jQuery);
1237 </script>
1238 <?php
1239 }
1240
1241 /**
1242 * Opens the support forum subemenu item in a new browser page.
1243 *
1244 * @author Vova Feldman (@svovaf)
1245 * @since 2.1.4
1246 */
1247 static function _open_support_forum_in_new_page() {
1248 ?>
1249 <script type="text/javascript">
1250 (function ($) {
1251 $('.fs-submenu-item.wp-support-forum').parent().attr('target', '_blank');
1252 })(jQuery);
1253 </script>
1254 <?php
1255 }
1256
1257 /**
1258 * @author Vova Feldman (@svovaf)
1259 * @since 1.0.9
1260 */
1261 private function _register_hooks() {
1262 $this->_logger->entrance();
1263
1264 if ( is_admin() ) {
1265 add_action( 'plugins_loaded', array( &$this, '_hook_action_links_and_register_account_hooks' ) );
1266
1267 if ( $this->is_plugin() ) {
1268 if ( self::is_plugin_install_page() && true !== fs_request_get_bool( 'fs_allow_updater_and_dialog' ) ) {
1269 /**
1270 * Unless the `fs_allow_updater_and_dialog` URL param exists and its value is `true`, make
1271 * Freemius-related updates unavailable on the "Add Plugins" admin page (/plugin-install.php)
1272 * so that they won't interfere with the .org plugins' functionalities on that page (e.g.
1273 * updating of a .org plugin).
1274 */
1275 add_filter( 'site_transient_update_plugins', array( 'Freemius', '_remove_fs_updates_from_plugin_install_page' ), 10, 2 );
1276 } else if ( self::is_plugins_page() || self::is_updates_page() ) {
1277 /**
1278 * On the "Plugins" and "Updates" admin pages, if there are premium or non–org-compliant
1279 * plugins, modify their details dialog URLs (add a Freemius-specific param) so that the SDK can
1280 * determine if the plugin information dialog should show information from Freemius.
1281 *
1282 * @author Leo Fajardo (@leorw)
1283 * @since 2.2.3
1284 */
1285 add_action( 'admin_footer', array( 'Freemius', '_prepend_fs_allow_updater_and_dialog_flag_url_param' ) );
1286 }
1287
1288 $plugin_dir = dirname( $this->_plugin_dir_path ) . '/';
1289
1290 /**
1291 * @since 1.2.2
1292 *
1293 * Hook to both free and premium version activations to support
1294 * auto deactivation on the other version activation.
1295 */
1296 register_activation_hook(
1297 $plugin_dir . $this->_free_plugin_basename,
1298 array( &$this, '_activate_plugin_event_hook' )
1299 );
1300
1301 register_activation_hook(
1302 $plugin_dir . $this->premium_plugin_basename(),
1303 array( &$this, '_activate_plugin_event_hook' )
1304 );
1305 } else {
1306 add_action( 'after_switch_theme', array( &$this, '_activate_theme_event_hook' ), 10, 2 );
1307
1308 /**
1309 * Include the required hooks to capture the theme settings' page tabs
1310 * and cache them.
1311 *
1312 * @author Vova Feldman (@svovaf)
1313 * @since 1.2.2.7
1314 */
1315 if ( ! $this->_cache->has_valid( 'tabs' ) ) {
1316 add_action( 'admin_footer', array( &$this, '_tabs_capture' ) );
1317 // Add license activation AJAX callback.
1318 $this->add_ajax_action( 'store_tabs', array( &$this, '_store_tabs_ajax_action' ) );
1319
1320 add_action( 'admin_enqueue_scripts', array( &$this, '_store_tabs_styles' ), 9999999 );
1321 }
1322
1323 add_action(
1324 'admin_footer',
1325 array( &$this, '_add_freemius_tabs' ),
1326 /**
1327 * The tabs JS code must be executed after the tabs capture logic (_tabs_capture()).
1328 * That's why the priority is 11 while the tabs capture logic is added
1329 * with priority 10.
1330 *
1331 * @author Vova Feldman (@svovaf)
1332 */
1333 11
1334 );
1335
1336 add_action( 'admin_footer', array( &$this, '_style_premium_theme' ) );
1337 }
1338
1339 /**
1340 * Part of the mechanism to identify new plugin install vs. plugin update.
1341 *
1342 * @author Vova Feldman (@svovaf)
1343 * @since 1.1.9
1344 */
1345 if ( empty( $this->_storage->was_plugin_loaded ) ) {
1346 /**
1347 * During the plugin activation (not theme), 'plugins_loaded' will be already executed
1348 * when the logic gets here since the activation logic first add the activate plugins,
1349 * then triggers 'plugins_loaded', and only then include the code of the plugin that
1350 * is activated. Which means that _plugins_loaded() will NOT be executed during the
1351 * plugin activation, and that IS intentional.
1352 *
1353 * @author Vova Feldman (@svovaf)
1354 */
1355 if ( $this->is_plugin() && $this->is_activation_mode( false ) ) {
1356 add_action( 'plugins_loaded', array( &$this, '_plugins_loaded' ) );
1357 } else {
1358 // If was activated before, then it was already loaded before.
1359 $this->_plugins_loaded();
1360 }
1361 }
1362
1363 if ( ! self::is_ajax() ) {
1364 if ( ! $this->is_addon() ) {
1365 add_action( 'init', array( &$this, '_add_default_submenu_items' ), WP_FS__LOWEST_PRIORITY );
1366 }
1367 }
1368
1369 if ( $this->_storage->handle_gdpr_admin_notice ) {
1370 add_action( 'init', array( &$this, '_maybe_show_gdpr_admin_notice' ) );
1371 }
1372
1373 add_action( 'init', array( &$this, '_maybe_add_gdpr_optin_ajax_handler') );
1374 }
1375
1376 if ( $this->is_plugin() ) {
1377 if ( $this->_is_network_active ) {
1378 add_action( 'wpmu_new_blog', array( $this, '_after_new_blog_callback' ), 10, 6 );
1379 }
1380
1381 register_deactivation_hook( $this->_plugin_main_file_path, array( &$this, '_deactivate_plugin_hook' ) );
1382 }
1383
1384 if ( is_multisite() ) {
1385 add_action( 'deactivate_blog', array( &$this, '_after_site_deactivated_callback' ) );
1386 add_action( 'archive_blog', array( &$this, '_after_site_deactivated_callback' ) );
1387 add_action( 'make_spam_blog', array( &$this, '_after_site_deactivated_callback' ) );
1388 add_action( 'deleted_blog', array( &$this, '_after_site_deleted_callback' ), 10, 2 );
1389
1390 add_action( 'activate_blog', array( &$this, '_after_site_reactivated_callback' ) );
1391 add_action( 'unarchive_blog', array( &$this, '_after_site_reactivated_callback' ) );
1392 add_action( 'make_ham_blog', array( &$this, '_after_site_reactivated_callback' ) );
1393 }
1394
1395 if ( $this->is_theme() &&
1396 self::is_customizer() &&
1397 $this->apply_filters( 'show_customizer_upsell', true )
1398 ) {
1399 // Register customizer upsell.
1400 add_action( 'customize_register', array( &$this, '_customizer_register' ) );
1401 }
1402
1403 add_action( 'admin_init', array( &$this, '_redirect_on_clicked_menu_link' ), WP_FS__LOWEST_PRIORITY );
1404
1405 if ( $this->is_theme() ) {
1406 add_action( 'admin_init', array( &$this, '_add_tracking_links' ) );
1407 }
1408
1409 add_action( 'admin_init', array( &$this, '_add_license_activation' ) );
1410 add_action( 'admin_init', array( &$this, '_add_premium_version_upgrade_selection' ) );
1411
1412 $this->add_ajax_action( 'update_billing', array( &$this, '_update_billing_ajax_action' ) );
1413 $this->add_ajax_action( 'start_trial', array( &$this, '_start_trial_ajax_action' ) );
1414
1415 if ( $this->_is_network_active && fs_is_network_admin() ) {
1416 $this->add_ajax_action( 'network_activate', array( &$this, '_network_activate_ajax_action' ) );
1417 }
1418
1419 $this->add_ajax_action( 'install_premium_version', array(
1420 &$this,
1421 '_install_premium_version_ajax_action'
1422 ) );
1423
1424 $this->add_ajax_action( 'submit_affiliate_application', array( &$this, '_submit_affiliate_application' ) );
1425
1426 $this->add_action( 'after_plans_sync', array( &$this, '_check_for_trial_plans' ) );
1427
1428 $this->add_action( 'sdk_version_update', array( &$this, '_sdk_version_update' ), WP_FS__DEFAULT_PRIORITY, 2 );
1429
1430 $this->add_action(
1431 'plugin_version_update',
1432 array( &$this, '_after_version_update' ),
1433 WP_FS__DEFAULT_PRIORITY,
1434 2
1435 );
1436 $this->add_filter( 'after_code_type_change', array( &$this, '_after_code_type_change' ) );
1437
1438 add_action( 'admin_init', array( &$this, '_add_trial_notice' ) );
1439 add_action( 'admin_init', array( &$this, '_add_affiliate_program_notice' ) );
1440 add_action( 'admin_enqueue_scripts', array( &$this, '_enqueue_common_css' ) );
1441
1442 /**
1443 * Handle request to reset anonymous mode for `get_reconnect_url()`.
1444 *
1445 * @author Vova Feldman (@svovaf)
1446 * @since 1.2.1.5
1447 */
1448 if ( fs_request_is_action( 'reset_anonymous_mode' ) &&
1449 $this->get_unique_affix() === fs_request_get( 'fs_unique_affix' )
1450 ) {
1451 add_action( 'admin_init', array( &$this, 'connect_again' ) );
1452 }
1453 }
1454
1455 /**
1456 * Makes Freemius-related updates unavailable on the "Add Plugins" admin page (/plugin-install.php) so that
1457 * they won't interfere with the .org plugins' functionalities on that page (e.g. updating of a .org plugin).
1458 *
1459 * @author Leo Fajardo (@leorw)
1460 * @since 2.2.3
1461 *
1462 * @param object $updates
1463 * @param string|null $transient
1464 *
1465 * @return object
1466 */
1467 static function _remove_fs_updates_from_plugin_install_page( $updates, $transient = null ) {
1468 if ( is_object( $updates ) && isset( $updates->response ) ) {
1469 foreach ( $updates->response as $file => $plugin ) {
1470 if ( false !== strpos( $plugin->package, 'api.freemius' ) ) {
1471 unset( $updates->response[ $file ] );
1472 }
1473 }
1474 }
1475
1476 return $updates;
1477 }
1478
1479 /**
1480 * Prepends the `fs_allow_updater_and_dialog` param to the plugin information URLs to tell the SDK to handle
1481 * the information that is shown on the plugin details dialog that is shown when the relevant link is clicked.
1482 *
1483 * @author Leo Fajardo (@leorw)
1484 * @since 2.2.3
1485 *
1486 * @return string
1487 */
1488 static function _prepend_fs_allow_updater_and_dialog_flag_url_param() {
1489 $slug_basename_map = array();
1490 foreach ( self::$_instances as $instance ) {
1491 if ( ! $instance->is_plugin() ) {
1492 continue;
1493 }
1494
1495 $slug_basename_map[ $instance->get_slug() ] = $instance->premium_plugin_basename();
1496 }
1497 ?>
1498 <script type="text/javascript">
1499 (function( $ ) {
1500 var slugBasenameMap = <?php echo json_encode( $slug_basename_map ) ?>;
1501 for ( var slug in slugBasenameMap ) {
1502 var basename = slugBasenameMap[ slug ];
1503
1504 // Try to get the plugin rows if on the "Plugins" page.
1505 var $pluginRows = $( '.wp-list-table.plugins tr[data-plugin="' + basename + '"]');
1506
1507 if ( 0 === $pluginRows.length ) {
1508 // Try to get the plugin rows if on the "Updates" page.
1509 var $pluginCheckbox = $( '#update-plugins-table input[type="checkbox"][value="' + basename + '"]' );
1510 if ( 0 !== $pluginCheckbox.length ) {
1511 $pluginRows = $pluginCheckbox.parents( 'tr:first' );
1512 }
1513 }
1514
1515 if ( 0 === $pluginRows.length ) {
1516 // No plugin rows found.
1517 continue;
1518 }
1519
1520 // Find the "View details" links and add the `fs_allow_updater_and_dialog` param to the URL.
1521 $pluginRows.find( 'a[href*="plugin-install.php?tab=plugin-information"]' ).each(function() {
1522 var $this = $( this ),
1523 href = $this.attr( 'href' ).replace( '?tab=', '?fs_allow_updater_and_dialog=true&tab=');
1524
1525 $this.attr( 'href', href );
1526 });
1527 }
1528 })( jQuery );
1529 </script>
1530 <?php
1531 }
1532
1533 /**
1534 * Keeping the uninstall hook registered for free or premium plugin version may result to a fatal error that
1535 * could happen when a user tries to uninstall either version while one of them is still active. Uninstalling a
1536 * plugin will trigger inclusion of the free or premium version and if one of them is active during the
1537 * uninstallation, a fatal error may occur in case the plugin's class or functions are already defined.
1538 *
1539 * @author Leo Fajardo (@leorw)
1540 *
1541 * @since 1.2.0
1542 */
1543 private function unregister_uninstall_hook() {
1544 $uninstallable_plugins = (array) get_option( 'uninstall_plugins' );
1545 unset( $uninstallable_plugins[ $this->_free_plugin_basename ] );
1546 unset( $uninstallable_plugins[ $this->premium_plugin_basename() ] );
1547
1548 update_option( 'uninstall_plugins', $uninstallable_plugins );
1549 }
1550
1551 /**
1552 * @since 1.2.0 Invalidate module's main file cache, otherwise, FS_Plugin_Updater will not fetch updates.
1553 *
1554 * @param bool $store_prev_path
1555 */
1556 private function clear_module_main_file_cache( $store_prev_path = true ) {
1557 if ( ! isset( $this->_storage->plugin_main_file ) ||
1558 empty( $this->_storage->plugin_main_file->path )
1559 ) {
1560 return;
1561 }
1562
1563 if ( ! $store_prev_path ) {
1564 /**
1565 * Storing the previous path is not needed when clearing the cache after an SDK version update since
1566 * the main purpose of the cache clearing in that event is to correct a wrong plugin main file path
1567 * which causes data mix-up between plugins (e.g. titles and versions of an add-on and its parent plugin).
1568 *
1569 * @author Leo Fajardo (@leorw)
1570 * @since 2.2.1
1571 */
1572 unset( $this->_storage->plugin_main_file->path );
1573 } else {
1574 $plugin_main_file = clone $this->_storage->plugin_main_file;
1575
1576 // Store cached path (2nd layer cache).
1577 $plugin_main_file->prev_path = $plugin_main_file->path;
1578
1579 // Clear cached path.
1580 unset( $plugin_main_file->path );
1581
1582 $this->_storage->plugin_main_file = $plugin_main_file;
1583 }
1584
1585 /**
1586 * Clear global cached path.
1587 *
1588 * @author Leo Fajardo (@leorw)
1589 * @since 1.2.2
1590 */
1591 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map' );
1592 unset( $id_slug_type_path_map[ $this->_module_id ]['path'] );
1593 self::$_accounts->set_option( 'id_slug_type_path_map', $id_slug_type_path_map, true );
1594 }
1595
1596 /**
1597 * @author Leo Fajardo (@leorw)
1598 * @since 2.0.0
1599 */
1600 function _hook_action_links_and_register_account_hooks() {
1601 add_action( 'admin_init', array( &$this, '_add_tracking_links' ) );
1602
1603 if ( self::is_plugins_page() && $this->is_plugin() ) {
1604 $this->hook_plugin_action_links();
1605 }
1606
1607 $this->_register_account_hooks();
1608 }
1609
1610 /**
1611 * @author Vova Feldman (@svovaf)
1612 * @since 1.0.9
1613 */
1614 private function _register_account_hooks() {
1615 if ( ! is_admin() ) {
1616 return;
1617 }
1618
1619 /**
1620 * Always show the deactivation feedback form since we added
1621 * automatic free version deactivation upon premium code activation.
1622 *
1623 * @since 1.2.1.6
1624 */
1625 $this->add_ajax_action(
1626 'submit_uninstall_reason',
1627 array( &$this, '_submit_uninstall_reason_action' )
1628 );
1629
1630 $this->add_ajax_action(
1631 'cancel_subscription_or_trial',
1632 array( &$this, 'cancel_subscription_or_trial_ajax_action' )
1633 );
1634
1635 if ( ! $this->is_addon() || $this->is_parent_plugin_installed() ) {
1636 if ( ( $this->is_plugin() && self::is_plugins_page() ) ||
1637 ( $this->is_theme() && self::is_themes_page() )
1638 ) {
1639 add_action( 'admin_footer', array( &$this, '_add_deactivation_feedback_dialog_box' ) );
1640 }
1641 }
1642 }
1643
1644 /**
1645 * Leverage backtrace to find caller plugin file path.
1646 *
1647 * @author Vova Feldman (@svovaf)
1648 * @since 1.0.6
1649 *
1650 * @param bool $is_init Is initiation sequence.
1651 *
1652 * @return string
1653 */
1654 private function _find_caller_plugin_file( $is_init = false ) {
1655 // Try to load the cached value of the file path.
1656 if ( isset( $this->_storage->plugin_main_file ) ) {
1657 $plugin_main_file = $this->_storage->plugin_main_file;
1658 if ( isset( $plugin_main_file->path ) ) {
1659 $absolute_path = $this->get_absolute_path( $plugin_main_file->path );
1660 if ( file_exists( $absolute_path ) ) {
1661 return $absolute_path;
1662 }
1663 }
1664 }
1665
1666 /**
1667 * @since 1.2.1
1668 *
1669 * `clear_module_main_file_cache()` is clearing the plugin's cached path on
1670 * deactivation. Therefore, if any plugin/theme was initiating `Freemius`
1671 * with that plugin's slug, it was overriding the empty plugin path with a wrong path.
1672 *
1673 * So, we've added a special mechanism with a 2nd layer of cache that uses `prev_path`
1674 * when the class instantiator isn't the module.
1675 */
1676 if ( ! $is_init ) {
1677 // Fetch prev path cache.
1678 if ( isset( $this->_storage->plugin_main_file ) &&
1679 isset( $this->_storage->plugin_main_file->prev_path )
1680 ) {
1681 $absolute_path = $this->get_absolute_path( $this->_storage->plugin_main_file->prev_path );
1682 if ( file_exists( $absolute_path ) ) {
1683 return $absolute_path;
1684 }
1685 }
1686
1687 wp_die(
1688 $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' ) .
1689 " Module: {$this->_slug}; SDK: " . WP_FS__SDK_VERSION . ";",
1690 $this->get_text_inline( 'Error', 'error' ),
1691 array( 'back_link' => true )
1692 );
1693 }
1694
1695 /**
1696 * @since 1.2.1
1697 *
1698 * Only the original instantiator that calls dynamic_init can modify the module's path.
1699 */
1700 // Find caller module.
1701 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
1702 $this->_storage->plugin_main_file = (object) array(
1703 'path' => $id_slug_type_path_map[ $this->_module_id ]['path'],
1704 );
1705
1706 return $this->get_absolute_path( $id_slug_type_path_map[ $this->_module_id ]['path'] );
1707 }
1708
1709 /**
1710 * @author Leo Fajardo (@leorw)
1711 * @since 1.2.3
1712 *
1713 * @param string $path
1714 *
1715 * @return string
1716 */
1717 private function get_relative_path( $path ) {
1718 $module_root_dir = $this->get_module_root_dir_path();
1719 if ( 0 === strpos( $path, $module_root_dir ) ) {
1720 $path = substr( $path, strlen( $module_root_dir ) );
1721 }
1722
1723 return $path;
1724 }
1725
1726 /**
1727 * @author Leo Fajardo (@leorw)
1728 * @since 1.2.3
1729 *
1730 * @param string $path
1731 * @param string|bool $module_type
1732 *
1733 * @return string
1734 */
1735 private function get_absolute_path( $path, $module_type = false ) {
1736 $module_root_dir = $this->get_module_root_dir_path( $module_type );
1737 if ( 0 !== strpos( $path, $module_root_dir ) ) {
1738 $path = fs_normalize_path( $module_root_dir . $path );
1739 }
1740
1741 return $path;
1742 }
1743
1744 /**
1745 * @author Leo Fajardo (@leorw)
1746 * @since 1.2.3
1747 *
1748 * @param string|bool $module_type
1749 *
1750 * @return string
1751 */
1752 private function get_module_root_dir_path( $module_type = false ) {
1753 $is_plugin = empty( $module_type ) ?
1754 $this->is_plugin() :
1755 ( WP_FS__MODULE_TYPE_PLUGIN === $module_type );
1756
1757 return fs_normalize_path( trailingslashit( $is_plugin ?
1758 WP_PLUGIN_DIR :
1759 get_theme_root( get_stylesheet() ) ) );
1760 }
1761
1762 /**
1763 * @author Leo Fajardo (@leorw)
1764 *
1765 * @param number $module_id
1766 * @param string $slug
1767 *
1768 * @since 1.2.2
1769 */
1770 private function store_id_slug_type_path_map( $module_id, $slug ) {
1771 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
1772
1773 $store_option = false;
1774
1775 if ( ! isset( $id_slug_type_path_map[ $module_id ] ) ) {
1776 $id_slug_type_path_map[ $module_id ] = array(
1777 'slug' => $slug
1778 );
1779
1780 $store_option = true;
1781 }
1782
1783 if ( ! isset( $id_slug_type_path_map[ $module_id ]['path'] ) ||
1784 /**
1785 * This verification is for cases when suddenly the same module
1786 * is installed but with a different folder name.
1787 *
1788 * @author Vova Feldman (@svovaf)
1789 * @since 1.2.3
1790 */
1791 ! file_exists( $this->get_absolute_path(
1792 $id_slug_type_path_map[ $module_id ]['path'],
1793 $id_slug_type_path_map[ $module_id ]['type']
1794 ) )
1795 ) {
1796 $caller_main_file_and_type = $this->get_caller_main_file_and_type();
1797
1798 $id_slug_type_path_map[ $module_id ]['type'] = $caller_main_file_and_type->module_type;
1799 $id_slug_type_path_map[ $module_id ]['path'] = $caller_main_file_and_type->path;
1800
1801 $store_option = true;
1802 }
1803
1804 if ( $store_option ) {
1805 self::$_accounts->set_option( 'id_slug_type_path_map', $id_slug_type_path_map, true );
1806 }
1807 }
1808
1809 /**
1810 * Identifies the caller type: plugin or theme.
1811 *
1812 * @author Leo Fajardo (@leorw)
1813 * @since 1.2.2
1814 *
1815 * @author Vova Feldman (@svovaf)
1816 * @since 1.2.2.3 Find the earliest module in the call stack that calls to the SDK. This fix is for cases when
1817 * add-ons are relying on loading the SDK from the parent module, and also allows themes including the
1818 * SDK an internal file instead of directly from functions.php.
1819 * @since 1.2.1.7 Knows how to handle cases when an add-on includes the parent module logic.
1820 */
1821 private function get_caller_main_file_and_type() {
1822 self::require_plugin_essentials();
1823
1824 $all_plugins = fs_get_plugins( true );
1825 $all_plugins_paths = array();
1826
1827 // Get active plugin's main files real full names (might be symlinks).
1828 foreach ( $all_plugins as $relative_path => $data ) {
1829 if ( false === strpos( fs_normalize_path( $relative_path ), '/' ) ) {
1830 /**
1831 * Ignore plugins that don't have a folder (e.g. Hello Dolly) since they
1832 * can't really include the SDK.
1833 *
1834 * @author Vova Feldman
1835 * @since 1.2.1.7
1836 */
1837 continue;
1838 }
1839
1840 $all_plugins_paths[] = fs_normalize_path( realpath( WP_PLUGIN_DIR . '/' . $relative_path ) );
1841 }
1842
1843 $caller_file_candidate = false;
1844 $caller_map = array();
1845 $module_type = WP_FS__MODULE_TYPE_PLUGIN;
1846 $themes_dir = fs_normalize_path( get_theme_root( get_stylesheet() ) );
1847
1848 for ( $i = 1, $bt = debug_backtrace(), $len = count( $bt ); $i < $len; $i ++ ) {
1849 if ( empty( $bt[ $i ]['file'] ) ) {
1850 continue;
1851 }
1852
1853 if ( $i > 1 && ! empty( $bt[ $i - 1 ]['file'] ) && $bt[ $i ]['file'] === $bt[ $i - 1 ]['file'] ) {
1854 // If file same as the prev file in the stack, skip it.
1855 continue;
1856 }
1857
1858 if ( ! empty( $bt[ $i ]['function'] ) && in_array( $bt[ $i ]['function'], array(
1859 'do_action',
1860 'apply_filter',
1861 // The string split is stupid, but otherwise, theme check
1862 // throws info notices.
1863 'requir' . 'e_once',
1864 'requir' . 'e',
1865 'includ' . 'e_once',
1866 'includ' . 'e',
1867 'install_and_activate_plugin',
1868 'try_activate_plugin',
1869 'activate_plugin'
1870 ) )
1871 ) {
1872 // Ignore call stack hooks and files inclusion.
1873 continue;
1874 }
1875
1876 $caller_file_path = fs_normalize_path( $bt[ $i ]['file'] );
1877
1878 if ( 'functions.php' === basename( $caller_file_path ) ) {
1879 /**
1880 * 1. Assumes that theme's starting execution file is functions.php.
1881 * 2. This complex logic fixes symlink issues (e.g. with Vargant).
1882 *
1883 * @author Vova Feldman (@svovaf)
1884 * @since 1.2.2.5
1885 */
1886
1887 if ( $caller_file_path == fs_normalize_path( realpath( trailingslashit( $themes_dir ) . basename( dirname( $caller_file_path ) ) . '/' . basename( $caller_file_path ) ) ) ) {
1888 $module_type = WP_FS__MODULE_TYPE_THEME;
1889
1890 /**
1891 * Relative path of the theme, e.g.:
1892 * `my-theme/functions.php`
1893 *
1894 * @author Leo Fajardo (@leorw)
1895 */
1896 $caller_file_candidate = basename( dirname( $caller_file_path ) ) .
1897 '/' .
1898 basename( $caller_file_path );
1899
1900 continue;
1901 }
1902 }
1903
1904 $caller_file_hash = md5( $caller_file_path );
1905
1906 if ( ! isset( $caller_map[ $caller_file_hash ] ) ) {
1907 foreach ( $all_plugins_paths as $plugin_path ) {
1908 if ( false !== strpos( $caller_file_path, fs_normalize_path( dirname( $plugin_path ) . '/' ) ) ) {
1909 $caller_map[ $caller_file_hash ] = fs_normalize_path( $plugin_path );
1910 break;
1911 }
1912 }
1913 }
1914
1915 if ( isset( $caller_map[ $caller_file_hash ] ) ) {
1916 $module_type = WP_FS__MODULE_TYPE_PLUGIN;
1917 $caller_file_candidate = plugin_basename( $caller_map[ $caller_file_hash ] );
1918 }
1919 }
1920
1921 return (object) array(
1922 'module_type' => $module_type,
1923 'path' => $caller_file_candidate
1924 );
1925 }
1926
1927 #----------------------------------------------------------------------------------
1928 #region Deactivation Feedback Form
1929 #----------------------------------------------------------------------------------
1930
1931 /**
1932 * Displays a confirmation and feedback dialog box when the user clicks on the "Deactivate" link on the plugins
1933 * page.
1934 *
1935 * @author Vova Feldman (@svovaf)
1936 * @author Leo Fajardo (@leorw)
1937 * @since 1.1.2
1938 */
1939 function _add_deactivation_feedback_dialog_box() {
1940 /* Check the type of user:
1941 * 1. Long-term (long-term)
1942 * 2. Non-registered and non-anonymous short-term (non-registered-and-non-anonymous-short-term).
1943 * 3. Short-term (short-term)
1944 */
1945 $is_long_term_user = true;
1946
1947 // Check if the site is at least 2 days old.
1948 $time_installed = $this->_storage->install_timestamp;
1949
1950 // Difference in seconds.
1951 $date_diff = time() - $time_installed;
1952
1953 // Convert seconds to days.
1954 $date_diff_days = floor( $date_diff / ( 60 * 60 * 24 ) );
1955
1956 if ( $date_diff_days < 2 ) {
1957 $is_long_term_user = false;
1958 }
1959
1960 $is_long_term_user = $this->apply_filters( 'is_long_term_user', $is_long_term_user );
1961
1962 if ( $is_long_term_user ) {
1963 $user_type = 'long-term';
1964 } else {
1965 if ( ! $this->is_registered() && ! $this->is_anonymous() ) {
1966 $user_type = 'non-registered-and-non-anonymous-short-term';
1967 } else {
1968 $user_type = 'short-term';
1969 }
1970 }
1971
1972 $uninstall_reasons = $this->_get_uninstall_reasons( $user_type );
1973
1974 // Load the HTML template for the deactivation feedback dialog box.
1975 $vars = array(
1976 'reasons' => $uninstall_reasons,
1977 'id' => $this->_module_id
1978 );
1979
1980 /**
1981 * @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.
1982 */
1983 fs_require_template( 'forms/deactivation/form.php', $vars );
1984 }
1985
1986 /**
1987 * @author Leo Fajardo (@leorw)
1988 * @since 1.1.2
1989 *
1990 * @param string $user_type
1991 *
1992 * @return array The uninstall reasons for the specified user type.
1993 */
1994 function _get_uninstall_reasons( $user_type = 'long-term' ) {
1995 $module_type = $this->_module_type;
1996
1997 $internal_message_template_var = array(
1998 'id' => $this->_module_id
1999 );
2000
2001 $plan = $this->get_plan();
2002
2003 if ( $this->is_registered() && is_object( $plan ) && $plan->has_technical_support() ) {
2004 $contact_support_template = fs_get_template( 'forms/deactivation/contact.php', $internal_message_template_var );
2005 } else {
2006 $contact_support_template = '';
2007 }
2008
2009 $reason_found_better_plugin = array(
2010 'id' => self::REASON_FOUND_A_BETTER_PLUGIN,
2011 'text' => sprintf( $this->get_text_inline( 'I found a better %s', 'reason-found-a-better-plugin' ), $module_type ),
2012 'input_type' => 'textfield',
2013 'input_placeholder' => sprintf( $this->get_text_inline( "What's the %s's name?", 'placeholder-plugin-name' ), $module_type ),
2014 );
2015
2016 $reason_temporary_deactivation = array(
2017 'id' => self::REASON_TEMPORARY_DEACTIVATION,
2018 'text' => sprintf(
2019 $this->get_text_inline( "It's a temporary %s. I'm just debugging an issue.", 'reason-temporary-x' ),
2020 strtolower( $this->is_plugin() ?
2021 $this->get_text_inline( 'Deactivation', 'deactivation' ) :
2022 $this->get_text_inline( 'Theme Switch', 'theme-switch' )
2023 )
2024 ),
2025 'input_type' => '',
2026 'input_placeholder' => ''
2027 );
2028
2029 $reason_other = array(
2030 'id' => self::REASON_OTHER,
2031 'text' => $this->get_text_inline( 'Other', 'reason-other' ),
2032 'input_type' => 'textfield',
2033 'input_placeholder' => ''
2034 );
2035
2036 $long_term_user_reasons = array(
2037 array(
2038 'id' => self::REASON_NO_LONGER_NEEDED,
2039 'text' => sprintf( $this->get_text_inline( 'I no longer need the %s', 'reason-no-longer-needed' ), $module_type ),
2040 'input_type' => '',
2041 'input_placeholder' => ''
2042 ),
2043 $reason_found_better_plugin,
2044 array(
2045 'id' => self::REASON_NEEDED_FOR_A_SHORT_PERIOD,
2046 'text' => sprintf( $this->get_text_inline( 'I only needed the %s for a short period', 'reason-needed-for-a-short-period' ), $module_type ),
2047 'input_type' => '',
2048 'input_placeholder' => ''
2049 ),
2050 array(
2051 'id' => self::REASON_BROKE_MY_SITE,
2052 'text' => sprintf( $this->get_text_inline( 'The %s broke my site', 'reason-broke-my-site' ), $module_type ),
2053 'input_type' => '',
2054 'input_placeholder' => '',
2055 'internal_message' => $contact_support_template
2056 ),
2057 array(
2058 'id' => self::REASON_SUDDENLY_STOPPED_WORKING,
2059 'text' => sprintf( $this->get_text_inline( 'The %s suddenly stopped working', 'reason-suddenly-stopped-working' ), $module_type ),
2060 'input_type' => '',
2061 'input_placeholder' => '',
2062 'internal_message' => $contact_support_template
2063 )
2064 );
2065
2066 if ( $this->is_paying() ) {
2067 $long_term_user_reasons[] = array(
2068 'id' => self::REASON_CANT_PAY_ANYMORE,
2069 'text' => $this->get_text_inline( "I can't pay for it anymore", 'reason-cant-pay-anymore' ),
2070 'input_type' => 'textfield',
2071 'input_placeholder' => $this->get_text_inline( 'What price would you feel comfortable paying?', 'placeholder-comfortable-price' )
2072 );
2073 }
2074
2075 $reason_dont_share_info = array(
2076 'id' => self::REASON_DONT_LIKE_TO_SHARE_MY_INFORMATION,
2077 'text' => $this->get_text_inline( "I don't like to share my information with you", 'reason-dont-like-to-share-my-information' ),
2078 'input_type' => '',
2079 'input_placeholder' => ''
2080 );
2081
2082 /**
2083 * If the current user has selected the "don't share data" reason in the deactivation feedback modal, inform the
2084 * user by showing additional message that he doesn't have to share data and can just choose to skip the opt-in
2085 * (the Skip button is included in the message to show). This message will only be shown if anonymous mode is
2086 * enabled and the user's account is currently not in pending activation state (similar to the way the Skip
2087 * button in the opt-in form is shown/hidden).
2088 */
2089 if ( $this->is_enable_anonymous() && ! $this->is_pending_activation() ) {
2090 $reason_dont_share_info['internal_message'] = fs_get_template( 'forms/deactivation/retry-skip.php', $internal_message_template_var );
2091 }
2092
2093 $uninstall_reasons = array(
2094 'long-term' => $long_term_user_reasons,
2095 'non-registered-and-non-anonymous-short-term' => array(
2096 array(
2097 'id' => self::REASON_DIDNT_WORK,
2098 'text' => sprintf( $this->get_text_inline( "The %s didn't work", 'reason-didnt-work' ), $module_type ),
2099 'input_type' => '',
2100 'input_placeholder' => ''
2101 ),
2102 $reason_dont_share_info,
2103 $reason_found_better_plugin
2104 ),
2105 'short-term' => array(
2106 array(
2107 'id' => self::REASON_COULDNT_MAKE_IT_WORK,
2108 'text' => $this->get_text_inline( "I couldn't understand how to make it work", 'reason-couldnt-make-it-work' ),
2109 'input_type' => '',
2110 'input_placeholder' => '',
2111 'internal_message' => $contact_support_template
2112 ),
2113 $reason_found_better_plugin,
2114 array(
2115 'id' => self::REASON_GREAT_BUT_NEED_SPECIFIC_FEATURE,
2116 '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 ),
2117 'input_type' => 'textarea',
2118 'input_placeholder' => $this->get_text_inline( 'What feature?', 'placeholder-feature' )
2119 ),
2120 array(
2121 'id' => self::REASON_NOT_WORKING,
2122 'text' => sprintf( $this->get_text_inline( 'The %s is not working', 'reason-not-working' ), $module_type ),
2123 'input_type' => 'textarea',
2124 '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' )
2125 ),
2126 array(
2127 'id' => self::REASON_NOT_WHAT_I_WAS_LOOKING_FOR,
2128 'text' => $this->get_text_inline( "It's not what I was looking for", 'reason-not-what-i-was-looking-for' ),
2129 'input_type' => 'textarea',
2130 'input_placeholder' => $this->get_text_inline( "What you've been looking for?", 'placeholder-what-youve-been-looking-for' )
2131 ),
2132 array(
2133 'id' => self::REASON_DIDNT_WORK_AS_EXPECTED,
2134 'text' => sprintf( $this->get_text_inline( "The %s didn't work as expected", 'reason-didnt-work-as-expected' ), $module_type ),
2135 'input_type' => 'textarea',
2136 'input_placeholder' => $this->get_text_inline( 'What did you expect?', 'placeholder-what-did-you-expect' )
2137 )
2138 )
2139 );
2140
2141 // Randomize the reasons for the current user type.
2142 shuffle( $uninstall_reasons[ $user_type ] );
2143
2144 // Keep the following reasons as the last items in the list.
2145 $uninstall_reasons[ $user_type ][] = $reason_temporary_deactivation;
2146 $uninstall_reasons[ $user_type ][] = $reason_other;
2147
2148 $uninstall_reasons = $this->apply_filters( 'uninstall_reasons', $uninstall_reasons );
2149
2150 return $uninstall_reasons[ $user_type ];
2151 }
2152
2153 /**
2154 * Called after the user has submitted his reason for deactivating the plugin.
2155 *
2156 * @author Leo Fajardo (@leorw)
2157 * @since 1.1.2
2158 */
2159 function _submit_uninstall_reason_action() {
2160 $this->_logger->entrance();
2161
2162 $this->check_ajax_referer( 'submit_uninstall_reason' );
2163
2164 $reason_id = fs_request_get( 'reason_id' );
2165
2166 // Check if the given reason ID is an unsigned integer.
2167 if ( ! ctype_digit( $reason_id ) ) {
2168 exit;
2169 }
2170
2171 $reason_info = trim( fs_request_get( 'reason_info', '' ) );
2172 if ( ! empty( $reason_info ) ) {
2173 $reason_info = substr( $reason_info, 0, 128 );
2174 }
2175
2176 $reason = (object) array(
2177 'id' => $reason_id,
2178 'info' => $reason_info,
2179 'is_anonymous' => fs_request_get_bool( 'is_anonymous' )
2180 );
2181
2182 $this->_storage->store( 'uninstall_reason', $reason );
2183
2184 /**
2185 * If the module type is "theme", trigger the uninstall event here (on theme deactivation) since themes do
2186 * not support uninstall hook.
2187 *
2188 * @author Leo Fajardo (@leorw)
2189 * @since 1.2.2
2190 */
2191 if ( $this->is_theme() ) {
2192 if ( $this->is_premium() && ! $this->has_active_valid_license() ) {
2193 FS_Plugin_Updater::instance( $this )->delete_update_data();
2194 }
2195
2196 $this->_uninstall_plugin_event( false );
2197 $this->remove_sdk_reference();
2198 }
2199
2200 // Print '1' for successful operation.
2201 echo 1;
2202 exit;
2203 }
2204
2205 /**
2206 * @author Leo Fajardo (@leorw)
2207 * @since 2.1.4
2208 */
2209 function cancel_subscription_or_trial_ajax_action() {
2210 $this->_logger->entrance();
2211
2212 $this->check_ajax_referer( 'cancel_subscription_or_trial' );
2213
2214 $result = $this->cancel_subscription_or_trial( fs_request_get( 'plugin_id', $this->get_id() ), false );
2215
2216 if ( $this->is_api_error( $result ) ) {
2217 $this->shoot_ajax_failure( $result->error->message );
2218 }
2219
2220 $this->shoot_ajax_success();
2221 }
2222
2223 /**
2224 * @author Leo Fajardo (@leorw)
2225 * @since 2.1.4
2226 *
2227 * @param number $plugin_id
2228 *
2229 * @return object
2230 */
2231 private function cancel_subscription_or_trial( $plugin_id ) {
2232 $fs = null;
2233 if ( $plugin_id == $this->get_id() ) {
2234 $fs = $this;
2235 } else if ( $this->is_addon_activated( $plugin_id ) ) {
2236 $fs = self::get_instance_by_id( $plugin_id );
2237 }
2238
2239 $result = null;
2240
2241 if ( ! is_null( $fs ) ) {
2242 $result = $fs->is_paid_trial() ?
2243 $fs->_cancel_trial() :
2244 $fs->_downgrade_site();
2245 }
2246
2247 return $result;
2248 }
2249
2250 /**
2251 * @author Leo Fajardo (@leorw)
2252 * @since 2.0.2
2253 */
2254 function _delete_theme_update_data_action() {
2255 FS_Plugin_Updater::instance( $this )->delete_update_data();
2256 }
2257
2258 #endregion
2259
2260 #----------------------------------------------------------------------------------
2261 #region Instance
2262 #----------------------------------------------------------------------------------
2263
2264 /**
2265 * Main singleton instance.
2266 *
2267 * @author Vova Feldman (@svovaf)
2268 * @since 1.0.0
2269 *
2270 * @param number $module_id
2271 * @param string|bool $slug
2272 * @param bool $is_init Is initiation sequence.
2273 *
2274 * @return Freemius|false
2275 */
2276 static function instance( $module_id, $slug = false, $is_init = false ) {
2277 if ( empty( $module_id ) ) {
2278 return false;
2279 }
2280
2281 /**
2282 * 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.
2283 */
2284 self::_load_required_static();
2285
2286 if ( ! is_numeric( $module_id ) ) {
2287 if ( ! $is_init && true === $slug ) {
2288 $is_init = true;
2289 }
2290
2291 $slug = $module_id;
2292
2293 $module = FS_Plugin_Manager::instance( $slug )->get();
2294
2295 if ( is_object( $module ) ) {
2296 $module_id = $module->id;
2297 }
2298 }
2299
2300 $key = 'm_' . $module_id;
2301
2302 if ( ! isset( self::$_instances[ $key ] ) ) {
2303 self::$_instances[ $key ] = new Freemius( $module_id, $slug, $is_init );
2304 }
2305
2306 return self::$_instances[ $key ];
2307 }
2308
2309 /**
2310 * @author Vova Feldman (@svovaf)
2311 * @since 1.0.6
2312 *
2313 * @param number $addon_id
2314 *
2315 * @return bool
2316 */
2317 private static function has_instance( $addon_id ) {
2318 return isset( self::$_instances[ 'm_' . $addon_id ] );
2319 }
2320
2321 /**
2322 * @author Leo Fajardo (@leorw)
2323 * @since 1.2.2
2324 *
2325 * @param string|number $id_or_slug
2326 * @param string $module_type
2327 *
2328 * @return number|false
2329 */
2330 private static function get_module_id( $id_or_slug, $module_type = WP_FS__MODULE_TYPE_PLUGIN ) {
2331 if ( is_numeric( $id_or_slug ) ) {
2332 return $id_or_slug;
2333 }
2334
2335 foreach ( self::$_instances as $instance ) {
2336 // Also check the module type since there can be a plugin and a theme with the same slug.
2337 if ( ( $module_type === $instance->get_module_type() ) && ( $id_or_slug === $instance->get_slug() ) ) {
2338 return $instance->get_id();
2339 }
2340 }
2341
2342 return false;
2343 }
2344
2345 /**
2346 * @author Vova Feldman (@svovaf)
2347 * @since 1.0.6
2348 *
2349 * @param number $id
2350 *
2351 * @return false|Freemius
2352 */
2353 static function get_instance_by_id( $id ) {
2354 return isset ( self::$_instances[ 'm_' . $id ] ) ?
2355 self::$_instances[ 'm_' . $id ] :
2356 false;
2357 }
2358
2359 /**
2360 *
2361 * @author Vova Feldman (@svovaf)
2362 * @since 1.0.1
2363 *
2364 * @param string $plugin_file
2365 * @param string $module_type
2366 *
2367 * @return false|Freemius
2368 */
2369 static function get_instance_by_file( $plugin_file, $module_type = WP_FS__MODULE_TYPE_PLUGIN ) {
2370 $slug = self::find_slug_by_basename( $plugin_file );
2371
2372 return ( false !== $slug ) ?
2373 self::instance( self::get_module_id( $slug, $module_type ) ) :
2374 false;
2375 }
2376
2377 /**
2378 * @author Vova Feldman (@svovaf)
2379 * @since 1.0.6
2380 *
2381 * @return false|Freemius
2382 */
2383 function get_parent_instance() {
2384 return self::get_instance_by_id( $this->_plugin->parent_plugin_id );
2385 }
2386
2387 /**
2388 * @author Vova Feldman (@svovaf)
2389 * @since 1.0.6
2390 *
2391 * @param string|number $id_or_slug
2392 *
2393 * @return false|Freemius
2394 */
2395 function get_addon_instance( $id_or_slug ) {
2396 $addon_id = self::get_module_id( $id_or_slug );
2397
2398 return self::instance( $addon_id );
2399 }
2400
2401 #endregion ------------------------------------------------------------------
2402
2403 /**
2404 * @author Vova Feldman (@svovaf)
2405 * @since 1.0.6
2406 *
2407 * @return bool
2408 */
2409 function is_parent_plugin_installed() {
2410 $is_active = self::has_instance( $this->_plugin->parent_plugin_id );
2411
2412 if ( $is_active ) {
2413 return true;
2414 }
2415
2416 /**
2417 * Parent module might be a theme. If that's the case, the add-on's FS
2418 * instance will be loaded prior to the theme's FS instance, therefore,
2419 * we need to check if it's active with a "look ahead".
2420 *
2421 * @author Vova Feldman
2422 * @since 1.2.2.3
2423 */
2424 global $fs_active_plugins;
2425 if ( is_object( $fs_active_plugins ) && is_array( $fs_active_plugins->plugins ) ) {
2426 $active_theme = wp_get_theme();
2427
2428 foreach ( $fs_active_plugins->plugins as $sdk => $module ) {
2429 if ( WP_FS__MODULE_TYPE_THEME === $module->type ) {
2430 if ( $module->plugin_path == $active_theme->get_stylesheet() ) {
2431 // Parent module is a theme and it's currently active.
2432 return true;
2433 }
2434 }
2435 }
2436 }
2437
2438 return false;
2439 }
2440
2441 /**
2442 * Check if add-on parent plugin in activation mode.
2443 *
2444 * @author Vova Feldman (@svovaf)
2445 * @since 1.0.7
2446 *
2447 * @return bool
2448 */
2449 function is_parent_in_activation() {
2450 $parent_fs = $this->get_parent_instance();
2451 if ( ! is_object( $parent_fs ) ) {
2452 return false;
2453 }
2454
2455 return ( $parent_fs->is_activation_mode() );
2456 }
2457
2458 /**
2459 * Is plugin in activation mode.
2460 *
2461 * @author Vova Feldman (@svovaf)
2462 * @since 1.0.7
2463 *
2464 * @param bool $and_on
2465 *
2466 * @return bool
2467 */
2468 function is_activation_mode( $and_on = true ) {
2469 return fs_is_network_admin() ?
2470 $this->is_network_activation_mode( $and_on ) :
2471 $this->is_site_activation_mode( $and_on );
2472 }
2473
2474 /**
2475 * Is plugin in activation mode.
2476 *
2477 * @author Vova Feldman (@svovaf)
2478 * @since 1.0.7
2479 *
2480 * @param bool $and_on
2481 *
2482 * @return bool
2483 */
2484 function is_site_activation_mode( $and_on = true ) {
2485 return (
2486 ( $this->is_on() || ! $and_on ) &&
2487 ( $this->is_premium() && true === $this->_storage->require_license_activation ) ||
2488 (
2489 ( ! $this->is_registered() ||
2490 ( $this->is_only_premium() && ! $this->has_features_enabled_license() ) ) &&
2491 ( ! $this->is_enable_anonymous() ||
2492 ( ! $this->is_anonymous() && ! $this->is_pending_activation() ) )
2493 )
2494 );
2495 }
2496
2497 /**
2498 * Checks if the SDK in network activation mode.
2499 *
2500 * @author Leo Fajardo (@leorw)
2501 * @since 2.0.0
2502 *
2503 * @param bool $and_on
2504 *
2505 * @return bool
2506 */
2507 private function is_network_activation_mode( $and_on = true ) {
2508 if ( ! $this->_is_network_active ) {
2509 // Not network activated.
2510 return false;
2511 }
2512
2513 if ( $this->is_network_upgrade_mode() ) {
2514 // Special flag to enforce network activation mode to decide what to do with the sites that are not yet opted-in nor skipped.
2515 return true;
2516 }
2517
2518 if ( ! $this->is_site_activation_mode( $and_on ) ) {
2519 // 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.
2520 return false;
2521 }
2522
2523 if ( $this->is_network_delegated_connection() ) {
2524 // Super-admin delegated the connection to the site admins -> not activation mode.
2525 return false;
2526 }
2527
2528 if ( $this->is_network_anonymous() && true !== $this->_storage->require_license_activation ) {
2529 // Super-admin skipped the connection network wide -> not activation mode.
2530 return false;
2531 }
2532
2533 if ( $this->is_network_registered() ) {
2534 // Super-admin connected at least one site -> not activation mode.
2535 return false;
2536 }
2537
2538 return true;
2539 }
2540
2541 /**
2542 * Check if current page is the opt-in/pending-activation page.
2543 *
2544 * @author Vova Feldman (@svovaf)
2545 * @since 1.2.1.7
2546 *
2547 * @return bool
2548 */
2549 function is_activation_page() {
2550 if ( $this->_menu->is_main_settings_page() ) {
2551 return true;
2552 }
2553
2554 if ( ! $this->is_activation_mode() ) {
2555 return false;
2556 }
2557
2558 // Check if current page is matching the activation page.
2559 return $this->is_matching_url( $this->get_activation_url() );
2560 }
2561
2562 /**
2563 * Check if URL path's are matching and that all querystring
2564 * arguments of the $sub_url exist in the $url with the same values.
2565 *
2566 * WARNING:
2567 * 1. This method doesn't check if the sub/domain are matching.
2568 * 2. Ignore case sensitivity.
2569 *
2570 * @author Vova Feldman (@svovaf)
2571 * @since 1.2.1.7
2572 *
2573 * @param string $sub_url
2574 * @param string $url If argument is not set, check if the sub_url matching the current's page URL.
2575 *
2576 * @return bool
2577 */
2578 private function is_matching_url( $sub_url, $url = '' ) {
2579 if ( empty( $url ) ) {
2580 $url = $_SERVER['REQUEST_URI'];
2581 }
2582
2583 $url = strtolower( $url );
2584 $sub_url = strtolower( $sub_url );
2585
2586 if ( parse_url( $sub_url, PHP_URL_PATH ) !== parse_url( $url, PHP_URL_PATH ) ) {
2587 // Different path - DO NOT OVERRIDE PAGE.
2588 return false;
2589 }
2590
2591 $url_params = array();
2592 parse_str( parse_url( $url, PHP_URL_QUERY ), $url_params );
2593
2594 $sub_url_params = array();
2595 parse_str( parse_url( $sub_url, PHP_URL_QUERY ), $sub_url_params );
2596
2597 foreach ( $sub_url_params as $key => $val ) {
2598 if ( ! isset( $url_params[ $key ] ) || $val != $url_params[ $key ] ) {
2599 // Not matching query string - DO NOT OVERRIDE PAGE.
2600 return false;
2601 }
2602 }
2603
2604 return true;
2605 }
2606
2607 /**
2608 * Get the basenames of all active plugins for specific blog. Including network activated plugins.
2609 *
2610 * @author Vova Feldman (@svovaf)
2611 * @since 2.0.0
2612 *
2613 * @param int $blog_id
2614 *
2615 * @return string[]
2616 */
2617 private static function get_active_plugins_basenames( $blog_id = 0 ) {
2618 if ( is_multisite() && $blog_id > 0 ) {
2619 $active_basenames = get_blog_option( $blog_id, 'active_plugins' );
2620 } else {
2621 $active_basenames = get_option( 'active_plugins' );
2622 }
2623
2624 if ( is_multisite() ) {
2625 $network_active_basenames = get_site_option( 'active_sitewide_plugins' );
2626
2627 if ( is_array( $network_active_basenames ) && ! empty( $network_active_basenames ) ) {
2628 $active_basenames = array_merge( $active_basenames, $network_active_basenames );
2629 }
2630 }
2631
2632 return $active_basenames;
2633 }
2634
2635 /**
2636 * Get collection of all active plugins. Including network activated plugins.
2637 *
2638 * @author Vova Feldman (@svovaf)
2639 * @since 1.0.9
2640 *
2641 * @param int $blog_id Since 2.0.0
2642 *
2643 * @return array[string]array
2644 */
2645 private static function get_active_plugins( $blog_id = 0 ) {
2646 self::require_plugin_essentials();
2647
2648 $active_plugin = array();
2649 $all_plugins = fs_get_plugins();
2650 $active_plugins_basenames = self::get_active_plugins_basenames( $blog_id );
2651
2652 foreach ( $active_plugins_basenames as $plugin_basename ) {
2653 $active_plugin[ $plugin_basename ] = $all_plugins[ $plugin_basename ];
2654 }
2655
2656 return $active_plugin;
2657 }
2658
2659 /**
2660 * Get collection of all site active plugins for a specified blog.
2661 *
2662 * @author Vova Feldman (@svovaf)
2663 * @since 2.0.0
2664 *
2665 * @param int $blog_id
2666 *
2667 * @return array[string]array
2668 */
2669 private static function get_site_active_plugins( $blog_id = 0 ) {
2670 $active_basenames = ( is_multisite() && $blog_id > 0 ) ?
2671 get_blog_option( $blog_id, 'active_plugins' ) :
2672 get_option( 'active_plugins' );
2673
2674 $active = array();
2675
2676 if ( ! is_array( $active_basenames ) ) {
2677 return $active;
2678 }
2679
2680 foreach ( $active_basenames as $basename ) {
2681 $active[ $basename ] = array(
2682 'is_active' => true,
2683 'Version' => '1.0', // Dummy version.
2684 'slug' => self::get_plugin_slug( $basename ),
2685 );
2686 }
2687
2688 return $active;
2689 }
2690
2691 /**
2692 * Get collection of all plugins with their activation status for a specified blog.
2693 *
2694 * @author Vova Feldman (@svovaf)
2695 * @since 1.1.8
2696 *
2697 * @param int $blog_id Since 2.0.0
2698 *
2699 * @return array Key is the plugin file path and the value is an array of the plugin data.
2700 */
2701 private static function get_all_plugins( $blog_id = 0 ) {
2702 self::require_plugin_essentials();
2703
2704 $all_plugins = fs_get_plugins();
2705
2706 $active_plugins_basenames = self::get_active_plugins_basenames( $blog_id );
2707
2708 foreach ( $all_plugins as $basename => &$data ) {
2709 // By default set to inactive (next foreach update the active plugins).
2710 $data['is_active'] = false;
2711 // Enrich with plugin slug.
2712 $data['slug'] = self::get_plugin_slug( $basename );
2713 }
2714
2715 // Flag active plugins.
2716 foreach ( $active_plugins_basenames as $basename ) {
2717 if ( isset( $all_plugins[ $basename ] ) ) {
2718 $all_plugins[ $basename ]['is_active'] = true;
2719 }
2720 }
2721
2722 return $all_plugins;
2723 }
2724
2725 /**
2726 * Get collection of all plugins and if they are network level activated.
2727 *
2728 * @author Vova Feldman (@svovaf)
2729 * @since 2.0.0
2730 *
2731 * @return array Key is the plugin basename and the value is an array of the plugin data.
2732 */
2733 private static function get_network_plugins() {
2734 self::require_plugin_essentials();
2735
2736 $all_plugins = fs_get_plugins();
2737
2738 $network_active_basenames = is_multisite() ?
2739 get_site_option( 'active_sitewide_plugins' ) :
2740 array();
2741
2742 foreach ( $all_plugins as $basename => &$data ) {
2743 // By default set to inactive (next foreach update the active plugins).
2744 $data['is_active'] = false;
2745 // Enrich with plugin slug.
2746 $data['slug'] = self::get_plugin_slug( $basename );
2747 }
2748
2749 // Flag active plugins.
2750 foreach ( $network_active_basenames as $basename ) {
2751 if ( isset( $all_plugins[ $basename ] ) ) {
2752 $all_plugins[ $basename ]['is_active'] = true;
2753 }
2754 }
2755
2756 return $all_plugins;
2757 }
2758
2759 /**
2760 * Cached result of get_site_transient( 'update_plugins' )
2761 *
2762 * @author Vova Feldman (@svovaf)
2763 * @since 1.1.8
2764 *
2765 * @var object
2766 */
2767 private static $_plugins_info;
2768
2769 /**
2770 * Helper function to get specified plugin's slug.
2771 *
2772 * @author Vova Feldman (@svovaf)
2773 * @since 1.1.8
2774 *
2775 * @param $basename
2776 *
2777 * @return string
2778 */
2779 private static function get_plugin_slug( $basename ) {
2780 if ( ! isset( self::$_plugins_info ) ) {
2781 self::$_plugins_info = get_site_transient( 'update_plugins' );
2782 }
2783
2784 $slug = '';
2785
2786 if ( is_object( self::$_plugins_info ) ) {
2787 if ( isset( self::$_plugins_info->no_update ) &&
2788 isset( self::$_plugins_info->no_update[ $basename ] ) &&
2789 ! empty( self::$_plugins_info->no_update[ $basename ]->slug )
2790 ) {
2791 $slug = self::$_plugins_info->no_update[ $basename ]->slug;
2792 } else if ( isset( self::$_plugins_info->response ) &&
2793 isset( self::$_plugins_info->response[ $basename ] ) &&
2794 ! empty( self::$_plugins_info->response[ $basename ]->slug )
2795 ) {
2796 $slug = self::$_plugins_info->response[ $basename ]->slug;
2797 }
2798 }
2799
2800 if ( empty( $slug ) ) {
2801 // Try to find slug from FS data.
2802 $slug = self::find_slug_by_basename( $basename );
2803 }
2804
2805 if ( empty( $slug ) ) {
2806 // Fallback to plugin's folder name.
2807 $slug = dirname( $basename );
2808 }
2809
2810 return $slug;
2811 }
2812
2813 private static $_statics_loaded = false;
2814
2815 /**
2816 * Load static resources.
2817 *
2818 * @author Vova Feldman (@svovaf)
2819 * @since 1.0.1
2820 */
2821 private static function _load_required_static() {
2822 if ( self::$_statics_loaded ) {
2823 return;
2824 }
2825
2826 self::$_static_logger = FS_Logger::get_logger( WP_FS__SLUG, WP_FS__DEBUG_SDK, WP_FS__ECHO_DEBUG_SDK );
2827
2828 self::$_static_logger->entrance();
2829
2830 self::$_accounts = FS_Options::instance( WP_FS__ACCOUNTS_OPTION_NAME, true );
2831
2832 if ( is_multisite() ) {
2833 $has_skipped_migration = (
2834 // 'id_slug_type_path_map' - was never stored on older versions, therefore, not exists on the site level.
2835 null === self::$_accounts->get_option( 'id_slug_type_path_map', null, false ) &&
2836 // 'file_slug_map' stored on the site level, so it was running an SDK version before it was integrated with MS-network.
2837 null !== self::$_accounts->get_option( 'file_slug_map', null, false )
2838 );
2839
2840 /**
2841 * If the file_slug_map exists on the site level but doesn't exist on the
2842 * network level storage, it means that we need to process the storage with migration.
2843 *
2844 * 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.
2845 *
2846 * @author Vova Feldman (@svovaf)
2847 * @since 2.0.0
2848 */
2849 if (
2850 ( $has_skipped_migration && true !== self::$_accounts->get_option( 'ms_migration_complete', false, true ) ) ||
2851 ( null === self::$_accounts->get_option( 'file_slug_map', null, true ) &&
2852 null !== self::$_accounts->get_option( 'file_slug_map', null, false ) )
2853 ) {
2854 self::migrate_options_to_network();
2855 }
2856 }
2857
2858 self::$_global_admin_notices = FS_Admin_Notices::instance( 'global' );
2859
2860 if ( ! WP_FS__DEMO_MODE ) {
2861 add_action( ( fs_is_network_admin() ? 'network_' : '' ) . 'admin_menu', array(
2862 'Freemius',
2863 '_add_debug_section'
2864 ) );
2865 }
2866
2867 add_action( "wp_ajax_fs_toggle_debug_mode", array( 'Freemius', '_toggle_debug_mode' ) );
2868
2869 self::add_ajax_action_static( 'get_debug_log', array( 'Freemius', '_get_debug_log' ) );
2870
2871 self::add_ajax_action_static( 'get_db_option', array( 'Freemius', '_get_db_option' ) );
2872
2873 self::add_ajax_action_static( 'set_db_option', array( 'Freemius', '_set_db_option' ) );
2874
2875 if ( 0 == did_action( 'plugins_loaded' ) ) {
2876 add_action( 'plugins_loaded', array( 'Freemius', '_load_textdomain' ), 1 );
2877 }
2878
2879 add_action( 'admin_footer', array( 'Freemius', '_enrich_ajax_url' ) );
2880 add_action( 'admin_footer', array( 'Freemius', '_open_support_forum_in_new_page' ) );
2881
2882
2883 self::$_statics_loaded = true;
2884 }
2885
2886 /**
2887 * @author Leo Fajardo (@leorw)
2888 *
2889 * @since 2.1.3
2890 */
2891 private static function migrate_options_to_network() {
2892 self::migrate_accounts_to_network();
2893
2894 // Migrate API options from site level to network level.
2895 $api_network_options = FS_Option_Manager::get_manager( WP_FS__OPTIONS_OPTION_NAME, true, true );
2896 $api_network_options->migrate_to_network();
2897
2898 // Migrate API cache to network level storage.
2899 FS_Cache_Manager::get_manager( WP_FS__API_CACHE_OPTION_NAME )->migrate_to_network();
2900
2901 self::$_accounts->set_option( 'ms_migration_complete', true, true );
2902 }
2903
2904 #----------------------------------------------------------------------------------
2905 #region Localization
2906 #----------------------------------------------------------------------------------
2907
2908 /**
2909 * Load framework's text domain.
2910 *
2911 * @author Vova Feldman (@svovaf)
2912 * @since 1.2.1
2913 */
2914 static function _load_textdomain() {
2915 if ( ! is_admin() ) {
2916 return;
2917 }
2918
2919 global $fs_active_plugins;
2920
2921 // Works both for plugins and themes.
2922 load_plugin_textdomain(
2923 'freemius',
2924 false,
2925 $fs_active_plugins->newest->sdk_path . '/languages/'
2926 );
2927 }
2928
2929 #endregion
2930
2931 #----------------------------------------------------------------------------------
2932 #region Debugging
2933 #----------------------------------------------------------------------------------
2934
2935 /**
2936 * @author Vova Feldman (@svovaf)
2937 * @since 1.0.8
2938 */
2939 static function _add_debug_section() {
2940 if ( ! is_super_admin() ) {
2941 // Add debug page only for super-admins.
2942 return;
2943 }
2944
2945 self::$_static_logger->entrance();
2946
2947 $title = sprintf( '%s [v.%s]', fs_text_inline( 'Freemius Debug' ), WP_FS__SDK_VERSION );
2948
2949 if ( WP_FS__DEV_MODE ) {
2950 // Add top-level debug menu item.
2951 $hook = FS_Admin_Menu_Manager::add_page(
2952 $title,
2953 $title,
2954 'manage_options',
2955 'freemius',
2956 array( 'Freemius', '_debug_page_render' )
2957 );
2958 } else {
2959 // Add hidden debug page.
2960 $hook = FS_Admin_Menu_Manager::add_subpage(
2961 null,
2962 $title,
2963 $title,
2964 'manage_options',
2965 'freemius',
2966 array( 'Freemius', '_debug_page_render' )
2967 );
2968 }
2969
2970 if ( ! empty( $hook ) ) {
2971 add_action( "load-$hook", array( 'Freemius', '_debug_page_actions' ) );
2972 }
2973 }
2974
2975 /**
2976 * @author Vova Feldman (@svovaf)
2977 * @since 1.1.7.3
2978 */
2979 static function _toggle_debug_mode() {
2980 if ( ! is_super_admin() ) {
2981 return;
2982 }
2983
2984 $is_on = fs_request_get( 'is_on', false, 'post' );
2985
2986 if ( fs_request_is_post() && in_array( $is_on, array( 0, 1 ) ) ) {
2987 update_option( 'fs_debug_mode', $is_on );
2988
2989 // Turn on/off storage logging.
2990 FS_Logger::_set_storage_logging( ( 1 == $is_on ) );
2991 }
2992
2993 exit;
2994 }
2995
2996 /**
2997 * @author Vova Feldman (@svovaf)
2998 * @since 1.2.1.6
2999 */
3000 static function _get_debug_log() {
3001 $logs = FS_Logger::load_db_logs(
3002 fs_request_get( 'filters', false, 'post' ),
3003 ! empty( $_POST['limit'] ) && is_numeric( $_POST['limit'] ) ? $_POST['limit'] : 200,
3004 ! empty( $_POST['offset'] ) && is_numeric( $_POST['offset'] ) ? $_POST['offset'] : 0
3005 );
3006
3007 self::shoot_ajax_success( $logs );
3008 }
3009
3010 /**
3011 * @author Vova Feldman (@svovaf)
3012 * @since 1.2.1.7
3013 */
3014 static function _get_db_option() {
3015 check_admin_referer( 'fs_get_db_option' );
3016
3017 $option_name = fs_request_get( 'option_name' );
3018
3019 if ( ! is_super_admin() ||
3020 ! fs_starts_with( $option_name, 'fs_' )
3021 ) {
3022 self::shoot_ajax_failure();
3023 }
3024
3025 $value = get_option( $option_name );
3026
3027 $result = array(
3028 'name' => $option_name,
3029 );
3030
3031 if ( false !== $value ) {
3032 if ( ! is_string( $value ) ) {
3033 $value = json_encode( $value );
3034 }
3035
3036 $result['value'] = $value;
3037 }
3038
3039 self::shoot_ajax_success( $result );
3040 }
3041
3042 /**
3043 * @author Vova Feldman (@svovaf)
3044 * @since 1.2.1.7
3045 */
3046 static function _set_db_option() {
3047 check_admin_referer( 'fs_set_db_option' );
3048
3049 $option_name = fs_request_get( 'option_name' );
3050
3051 if ( ! is_super_admin() ||
3052 ! fs_starts_with( $option_name, 'fs_' )
3053 ) {
3054 self::shoot_ajax_failure();
3055 }
3056
3057 $option_value = fs_request_get( 'option_value' );
3058
3059 if ( ! empty( $option_value ) ) {
3060 update_option( $option_name, $option_value );
3061 }
3062
3063 self::shoot_ajax_success();
3064 }
3065
3066 /**
3067 * @author Vova Feldman (@svovaf)
3068 * @since 1.0.8
3069 */
3070 static function _debug_page_actions() {
3071 self::_clean_admin_content_section();
3072
3073 if ( fs_request_is_action( 'restart_freemius' ) ) {
3074 check_admin_referer( 'restart_freemius' );
3075
3076 if ( ! is_multisite() ) {
3077 // Clear accounts data.
3078 self::$_accounts->clear( null, true );
3079 } else {
3080 $sites = self::get_sites();
3081 foreach ( $sites as $site ) {
3082 $blog_id = self::get_site_blog_id( $site );
3083 self::$_accounts->clear( $blog_id, true );
3084 }
3085
3086 // Clear network level storage.
3087 self::$_accounts->clear( true, true );
3088 }
3089
3090 // Clear SDK reference cache.
3091 delete_option( 'fs_active_plugins' );
3092 } else if ( fs_request_is_action( 'clear_updates_data' ) ) {
3093 check_admin_referer( 'clear_updates_data' );
3094
3095 if ( ! is_multisite() ) {
3096 set_site_transient( 'update_plugins', null );
3097 set_site_transient( 'update_themes', null );
3098 } else {
3099 $current_blog_id = get_current_blog_id();
3100
3101 $sites = self::get_sites();
3102 foreach ( $sites as $site ) {
3103 switch_to_blog( self::get_site_blog_id( $site ) );
3104
3105 set_site_transient( 'update_plugins', null );
3106 set_site_transient( 'update_themes', null );
3107 }
3108
3109 switch_to_blog( $current_blog_id );
3110 }
3111 } else if ( fs_request_is_action( 'simulate_trial' ) ) {
3112 check_admin_referer( 'simulate_trial' );
3113
3114 $fs = freemius( fs_request_get( 'module_id' ) );
3115
3116 // Update SDK install to at least 24 hours before.
3117 $fs->_storage->install_timestamp = ( time() - WP_FS__TIME_24_HOURS_IN_SEC );
3118 // Unset the trial shown timestamp.
3119 unset( $fs->_storage->trial_promotion_shown );
3120 } else if ( fs_request_is_action( 'simulate_network_upgrade' ) ) {
3121 check_admin_referer( 'simulate_network_upgrade' );
3122
3123 $fs = freemius( fs_request_get( 'module_id' ) );
3124
3125 self::set_network_upgrade_mode( $fs->_storage );
3126 } else if ( fs_request_is_action( 'delete_install' ) ) {
3127 check_admin_referer( 'delete_install' );
3128
3129 self::_delete_site_by_slug(
3130 fs_request_get( 'slug' ),
3131 fs_request_get( 'module_type' ),
3132 true,
3133 fs_request_get( 'blog_id', null )
3134 );
3135 } else if ( fs_request_is_action( 'delete_user' ) ) {
3136 check_admin_referer( 'delete_user' );
3137
3138 self::delete_user( fs_request_get( 'user_id' ) );
3139 } else if ( fs_request_is_action( 'download_logs' ) ) {
3140 check_admin_referer( 'download_logs' );
3141
3142 $download_url = FS_Logger::download_db_logs(
3143 fs_request_get( 'filters', false, 'post' )
3144 );
3145
3146 if ( false === $download_url ) {
3147 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.' );
3148 }
3149
3150 fs_redirect( $download_url );
3151 } else if ( fs_request_is_action( 'migrate_options_to_network' ) ) {
3152 check_admin_referer( 'migrate_options_to_network' );
3153
3154 self::migrate_options_to_network();
3155 }
3156 }
3157
3158 /**
3159 * @author Vova Feldman (@svovaf)
3160 * @since 1.0.8
3161 */
3162 static function _debug_page_render() {
3163 self::$_static_logger->entrance();
3164
3165 if ( ! is_multisite() ) {
3166 $all_plugins_installs = self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN );
3167 $all_themes_installs = self::get_all_sites( WP_FS__MODULE_TYPE_THEME );
3168 } else {
3169 $sites = self::get_sites();
3170
3171 $all_plugins_installs = array();
3172 $all_themes_installs = array();
3173
3174 foreach ( $sites as $site ) {
3175 $blog_id = self::get_site_blog_id( $site );
3176
3177 $plugins_installs = self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN, $blog_id );
3178
3179 foreach ( $plugins_installs as $slug => $install ) {
3180 if ( ! isset( $all_plugins_installs[ $slug ] ) ) {
3181 $all_plugins_installs[ $slug ] = array();
3182 }
3183
3184 $install->blog_id = $blog_id;
3185
3186 $all_plugins_installs[ $slug ][] = $install;
3187 }
3188
3189 $themes_installs = self::get_all_sites( WP_FS__MODULE_TYPE_THEME, $blog_id );
3190
3191 foreach ( $themes_installs as $slug => $install ) {
3192 if ( ! isset( $all_themes_installs[ $slug ] ) ) {
3193 $all_themes_installs[ $slug ] = array();
3194 }
3195
3196 $install->blog_id = $blog_id;
3197
3198 $all_themes_installs[ $slug ][] = $install;
3199 }
3200 }
3201 }
3202
3203 $licenses_by_module_type = self::get_all_licenses_by_module_type();
3204
3205 $vars = array(
3206 'plugin_sites' => $all_plugins_installs,
3207 'theme_sites' => $all_themes_installs,
3208 'users' => self::get_all_users(),
3209 'addons' => self::get_all_addons(),
3210 'account_addons' => self::get_all_account_addons(),
3211 'plugin_licenses' => $licenses_by_module_type[ WP_FS__MODULE_TYPE_PLUGIN ],
3212 'theme_licenses' => $licenses_by_module_type[ WP_FS__MODULE_TYPE_THEME ]
3213 );
3214
3215 fs_enqueue_local_style( 'fs_debug', '/admin/debug.css' );
3216 fs_require_once_template( 'debug.php', $vars );
3217 }
3218
3219 #endregion
3220
3221 #----------------------------------------------------------------------------------
3222 #region Connectivity Issues
3223 #----------------------------------------------------------------------------------
3224
3225 /**
3226 * Check if Freemius should be turned on for the current plugin install.
3227 *
3228 * Note:
3229 * $this->_is_on is updated in has_api_connectivity()
3230 *
3231 * @author Vova Feldman (@svovaf)
3232 * @since 1.0.9
3233 *
3234 * @return bool
3235 */
3236 function is_on() {
3237 self::$_static_logger->entrance();
3238
3239 if ( isset( $this->_is_on ) ) {
3240 return $this->_is_on;
3241 }
3242
3243 // If already installed or pending then sure it's on :)
3244 if ( $this->is_registered() || $this->is_pending_activation() ) {
3245 $this->_is_on = true;
3246
3247 return true;
3248 }
3249
3250 return false;
3251 }
3252
3253 /**
3254 * @author Vova Feldman (@svovaf)
3255 * @since 1.1.7.3
3256 *
3257 * @param bool $flush_if_no_connectivity
3258 *
3259 * @return bool
3260 */
3261 private function should_run_connectivity_test( $flush_if_no_connectivity = false ) {
3262 if ( ! isset( $this->_storage->connectivity_test ) ) {
3263 // Connectivity test was never executed, or cache was cleared.
3264 return true;
3265 }
3266
3267 if ( WP_FS__PING_API_ON_IP_OR_HOST_CHANGES ) {
3268 if ( WP_FS__IS_HTTP_REQUEST ) {
3269 if ( $_SERVER['HTTP_HOST'] != $this->_storage->connectivity_test['host'] ) {
3270 // Domain changed.
3271 return true;
3272 }
3273
3274 if ( WP_FS__REMOTE_ADDR != $this->_storage->connectivity_test['server_ip'] ) {
3275 // Server IP changed.
3276 return true;
3277 }
3278 }
3279 }
3280
3281 if ( $this->_storage->connectivity_test['is_connected'] &&
3282 $this->_storage->connectivity_test['is_active']
3283 ) {
3284 // API connected and Freemius is active - no need to run connectivity check.
3285 return false;
3286 }
3287
3288 if ( $flush_if_no_connectivity ) {
3289 /**
3290 * If explicitly asked to flush when no connectivity - do it only
3291 * if at least 10 sec passed from the last API connectivity test.
3292 */
3293 return ( isset( $this->_storage->connectivity_test['timestamp'] ) &&
3294 ( WP_FS__SCRIPT_START_TIME - $this->_storage->connectivity_test['timestamp'] ) > 10 );
3295 }
3296
3297 /**
3298 * @since 1.1.7 Don't check for connectivity on plugin downgrade.
3299 */
3300 $version = $this->get_plugin_version();
3301 if ( version_compare( $version, $this->_storage->connectivity_test['version'], '>' ) ) {
3302 // If it's a plugin version upgrade and Freemius is off or no connectivity, run connectivity test.
3303 return true;
3304 }
3305
3306 return false;
3307 }
3308
3309 /**
3310 * @author Vova Feldman (@svovaf)
3311 * @since 1.1.7.4
3312 *
3313 * @param int|null $blog_id Since 2.0.0.
3314 * @param bool $is_gdpr_test Since 2.0.2. Perform only the GDPR test.
3315 *
3316 * @return object|false
3317 */
3318 private function ping( $blog_id = null, $is_gdpr_test = false ) {
3319 if ( WP_FS__SIMULATE_NO_API_CONNECTIVITY ) {
3320 return false;
3321 }
3322
3323 $version = $this->get_plugin_version();
3324
3325 $is_update = $this->apply_filters( 'is_plugin_update', $this->is_plugin_update() );
3326
3327 return $this->get_api_plugin_scope()->ping(
3328 $this->get_anonymous_id( $blog_id ),
3329 array(
3330 'is_update' => json_encode( $is_update ),
3331 'version' => $version,
3332 'sdk' => $this->version,
3333 'is_admin' => json_encode( is_admin() ),
3334 'is_ajax' => json_encode( self::is_ajax() ),
3335 'is_cron' => json_encode( self::is_cron() ),
3336 'is_gdpr_test' => $is_gdpr_test,
3337 'is_http' => json_encode( WP_FS__IS_HTTP_REQUEST ),
3338 )
3339 );
3340 }
3341
3342 /**
3343 * Check if there's any connectivity issue to Freemius API.
3344 *
3345 * @author Vova Feldman (@svovaf)
3346 * @since 1.0.9
3347 *
3348 * @param bool $flush_if_no_connectivity
3349 *
3350 * @return bool
3351 */
3352 function has_api_connectivity( $flush_if_no_connectivity = false ) {
3353 $this->_logger->entrance();
3354
3355 if ( isset( $this->_has_api_connection ) && ( $this->_has_api_connection || ! $flush_if_no_connectivity ) ) {
3356 return $this->_has_api_connection;
3357 }
3358
3359 if ( WP_FS__SIMULATE_NO_API_CONNECTIVITY &&
3360 isset( $this->_storage->connectivity_test ) &&
3361 true === $this->_storage->connectivity_test['is_connected']
3362 ) {
3363 unset( $this->_storage->connectivity_test );
3364 }
3365
3366 if ( ! $this->should_run_connectivity_test( $flush_if_no_connectivity ) ) {
3367 $this->_has_api_connection = $this->_storage->connectivity_test['is_connected'];
3368 /**
3369 * @since 1.1.6 During dev mode, if there's connectivity - turn Freemius on regardless the configuration.
3370 *
3371 * @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.
3372 */
3373 $this->_is_on = $this->_storage->connectivity_test['is_active'] ||
3374 $this->is_premium() ||
3375 ( WP_FS__DEV_MODE && $this->_has_api_connection && ! WP_FS__SIMULATE_FREEMIUS_OFF );
3376
3377 return $this->_has_api_connection;
3378 }
3379
3380 $pong = $this->ping();
3381 $is_connected = $this->get_api_plugin_scope()->is_valid_ping( $pong );
3382
3383 if ( ! $is_connected ) {
3384 // API failure.
3385 $this->_add_connectivity_issue_message( $pong );
3386 }
3387
3388 if ( $is_connected ) {
3389 FS_GDPR_Manager::instance()->store_is_required( $pong->is_gdpr_required );
3390 }
3391
3392 $this->store_connectivity_info( $pong, $is_connected );
3393
3394 return $this->_has_api_connection;
3395 }
3396
3397 /**
3398 * @author Vova Feldman (@svovaf)
3399 * @since 1.1.7.4
3400 *
3401 * @param object $pong
3402 * @param bool $is_connected
3403 */
3404 private function store_connectivity_info( $pong, $is_connected ) {
3405 $this->_logger->entrance();
3406
3407 $version = $this->get_plugin_version();
3408
3409 if ( ! $is_connected || WP_FS__SIMULATE_FREEMIUS_OFF ) {
3410 $is_active = false;
3411 } else {
3412 $is_active = ( isset( $pong->is_active ) && true == $pong->is_active );
3413 }
3414
3415 $is_active = $this->apply_filters(
3416 'is_on',
3417 $is_active,
3418 $this->is_plugin_update(),
3419 $version
3420 );
3421
3422 $this->_storage->connectivity_test = array(
3423 'is_connected' => $is_connected,
3424 'host' => $_SERVER['HTTP_HOST'],
3425 'server_ip' => WP_FS__REMOTE_ADDR,
3426 'is_active' => $is_active,
3427 'timestamp' => WP_FS__SCRIPT_START_TIME,
3428 // Last version with connectivity attempt.
3429 'version' => $version,
3430 );
3431
3432 $this->_has_api_connection = $is_connected;
3433 $this->_is_on = $is_active || ( WP_FS__DEV_MODE && $is_connected && ! WP_FS__SIMULATE_FREEMIUS_OFF );
3434 }
3435
3436 /**
3437 * Force turning Freemius on.
3438 *
3439 * @author Vova Feldman (@svovaf)
3440 * @since 1.1.8.1
3441 *
3442 * @return bool TRUE if successfully turned on.
3443 */
3444 private function turn_on() {
3445 $this->_logger->entrance();
3446
3447 if ( $this->is_on() || ! isset( $this->_storage->connectivity_test['is_active'] ) ) {
3448 return false;
3449 }
3450
3451 $updated_connectivity = $this->_storage->connectivity_test;
3452 $updated_connectivity['is_active'] = true;
3453 $updated_connectivity['timestamp'] = WP_FS__SCRIPT_START_TIME;
3454 $this->_storage->connectivity_test = $updated_connectivity;
3455
3456 $this->_is_on = true;
3457
3458 return true;
3459 }
3460
3461 /**
3462 * Anonymous and unique site identifier (Hash).
3463 *
3464 * @author Vova Feldman (@svovaf)
3465 * @since 1.1.0
3466 *
3467 * @param null|int $blog_id Since 2.0.0
3468 *
3469 * @return string
3470 */
3471 function get_anonymous_id( $blog_id = null ) {
3472 $unique_id = self::$_accounts->get_option( 'unique_id', null, $blog_id );
3473
3474 if ( empty( $unique_id ) || ! is_string( $unique_id ) ) {
3475 $key = fs_strip_url_protocol( get_site_url( $blog_id ) );
3476
3477 $secure_auth = SECURE_AUTH_KEY;
3478 if ( empty( $secure_auth ) || false !== strpos( $secure_auth, ' ' ) ) {
3479 // Protect against default auth key.
3480 $secure_auth = md5( microtime() );
3481 }
3482
3483 /**
3484 * Base the unique identifier on the WP secure authentication key. Which
3485 * turns the key into a secret anonymous identifier. This will help us
3486 * to avoid duplicate installs generation on the backend upon opt-in.
3487 *
3488 * @author Vova Feldman (@svovaf)
3489 * @since 1.2.3
3490 */
3491 $unique_id = md5( $key . $secure_auth );
3492
3493 self::$_accounts->set_option( 'unique_id', $unique_id, true, $blog_id );
3494 }
3495
3496 $this->_logger->departure( $unique_id );
3497
3498 return $unique_id;
3499 }
3500
3501 /**
3502 * @author Vova Feldman (@svovaf)
3503 * @since 1.1.7.4
3504 *
3505 * @return \WP_User
3506 */
3507 static function _get_current_wp_user() {
3508 self::require_pluggable_essentials();
3509 self::wp_cookie_constants();
3510
3511 return wp_get_current_user();
3512 }
3513
3514 /**
3515 * Define cookie constants which are required by Freemius::_get_current_wp_user() since
3516 * it uses wp_get_current_user() which needs the cookie constants set. When a plugin
3517 * is network activated the cookie constants are only configured after the network
3518 * plugins activation, therefore, if we don't define those constants WP will throw
3519 * PHP warnings/notices.
3520 *
3521 * @author Vova Feldman (@svovaf)
3522 * @since 2.1.1
3523 */
3524 private static function wp_cookie_constants() {
3525 if ( defined( 'LOGGED_IN_COOKIE' ) &&
3526 ( defined( 'AUTH_COOKIE' ) || defined( 'SECURE_AUTH_COOKIE' ) )
3527 ) {
3528 return;
3529 }
3530
3531 /**
3532 * Used to guarantee unique hash cookies
3533 *
3534 * @since 1.5.0
3535 */
3536 if ( ! defined( 'COOKIEHASH' ) ) {
3537 $siteurl = get_site_option( 'siteurl' );
3538 if ( $siteurl ) {
3539 define( 'COOKIEHASH', md5( $siteurl ) );
3540 } else {
3541 define( 'COOKIEHASH', '' );
3542 }
3543 }
3544
3545 if ( ! defined( 'LOGGED_IN_COOKIE' ) ) {
3546 define( 'LOGGED_IN_COOKIE', 'wordpress_logged_in_' . COOKIEHASH );
3547 }
3548
3549 /**
3550 * @since 2.5.0
3551 */
3552 if ( ! defined( 'AUTH_COOKIE' ) ) {
3553 define( 'AUTH_COOKIE', 'wordpress_' . COOKIEHASH );
3554 }
3555
3556 /**
3557 * @since 2.6.0
3558 */
3559 if ( ! defined( 'SECURE_AUTH_COOKIE' ) ) {
3560 define( 'SECURE_AUTH_COOKIE', 'wordpress_sec_' . COOKIEHASH );
3561 }
3562 }
3563
3564 /**
3565 * @author Vova Feldman (@svovaf)
3566 * @since 2.1.0
3567 *
3568 * @return int
3569 */
3570 static function get_current_wp_user_id() {
3571 $wp_user = self::_get_current_wp_user();
3572
3573 return $wp_user->ID;
3574 }
3575
3576 /**
3577 * @author Vova Feldman (@svovaf)
3578 * @since 1.2.1.7
3579 *
3580 * @param string $email
3581 *
3582 * @return bool
3583 */
3584 static function is_valid_email( $email ) {
3585 if ( false === filter_var( $email, FILTER_VALIDATE_EMAIL ) ) {
3586 return false;
3587 }
3588
3589 $parts = explode( '@', $email );
3590
3591 if ( 2 !== count( $parts ) || empty( $parts[1] ) ) {
3592 return false;
3593 }
3594
3595 $blacklist = array(
3596 'admin.',
3597 'webmaster.',
3598 'localhost.',
3599 'dev.',
3600 'development.',
3601 'test.',
3602 'stage.',
3603 'staging.',
3604 );
3605
3606 // Make sure domain is not one of the blacklisted.
3607 foreach ( $blacklist as $invalid ) {
3608 if ( 0 === strpos( $parts[1], $invalid ) ) {
3609 return false;
3610 }
3611 }
3612
3613 // Get the UTF encoded domain name.
3614 $domain = idn_to_ascii( $parts[1] ) . '.';
3615
3616 return ( checkdnsrr( $domain, 'MX' ) || checkdnsrr( $domain, 'A' ) );
3617 }
3618
3619 /**
3620 * Generate API connectivity issue message.
3621 *
3622 * @author Vova Feldman (@svovaf)
3623 * @since 1.0.9
3624 *
3625 * @param mixed $api_result
3626 * @param bool $is_first_failure
3627 */
3628 function _add_connectivity_issue_message( $api_result, $is_first_failure = true ) {
3629 if ( ! $this->is_premium() && $this->_enable_anonymous ) {
3630 // Don't add message if it's the free version and can run anonymously.
3631 return;
3632 }
3633
3634 if ( ! function_exists( 'wp_nonce_url' ) ) {
3635 require_once ABSPATH . 'wp-includes/functions.php';
3636 }
3637
3638 $current_user = self::_get_current_wp_user();
3639 // $admin_email = get_option( 'admin_email' );
3640 $admin_email = $current_user->user_email;
3641
3642 // Aliases.
3643 $deactivate_plugin_title = $this->esc_html_inline( 'That\'s exhausting, please deactivate', 'deactivate-plugin-title' );
3644 $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' );
3645 $install_previous_title = $this->esc_html_inline( 'Let\'s try your previous version', 'install-previous-title' );
3646 $install_previous_desc = $this->esc_html_inline( 'Uninstall this version and install the previous one.', 'install-previous-desc' );
3647 $fix_issue_title = $this->esc_html_inline( 'Yes - I\'m giving you a chance to fix it', 'fix-issue-title' );
3648 $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' );
3649 /* translators: %s: product title (e.g. "Awesome Plugin" requires an access to...) */
3650 $x_requires_access_to_api = $this->esc_html_inline( '%s requires an access to our API.', 'x-requires-access-to-api' );
3651 $sysadmin_title = $this->esc_html_inline( 'I\'m a system administrator', 'sysadmin-title' );
3652 $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' );
3653
3654 $message = false;
3655 if ( is_object( $api_result ) &&
3656 isset( $api_result->error ) &&
3657 isset( $api_result->error->code )
3658 ) {
3659 switch ( $api_result->error->code ) {
3660 case 'curl_missing':
3661 $missing_methods = '';
3662 if ( is_array( $api_result->missing_methods ) &&
3663 ! empty( $api_result->missing_methods )
3664 ) {
3665 foreach ( $api_result->missing_methods as $m ) {
3666 if ( 'curl_version' === $m ) {
3667 continue;
3668 }
3669
3670 if ( ! empty( $missing_methods ) ) {
3671 $missing_methods .= ', ';
3672 }
3673
3674 $missing_methods .= sprintf( '<code>%s</code>', $m );
3675 }
3676
3677 if ( ! empty( $missing_methods ) ) {
3678 $missing_methods = sprintf(
3679 '<br><br><b>%s</b> %s',
3680 $this->esc_html_inline( 'Disabled method(s):', 'curl-disabled-methods' ),
3681 $missing_methods
3682 );
3683 }
3684 }
3685
3686 $message = sprintf(
3687 $x_requires_access_to_api . ' ' .
3688 $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' ) . ' ' .
3689 $missing_methods .
3690 ' %s',
3691 '<b>' . $this->get_plugin_name() . '</b>',
3692 sprintf(
3693 '<ol id="fs_firewall_issue_options"><li>%s</li><li>%s</li><li>%s</li></ol>',
3694 sprintf(
3695 '<a class="fs-resolve" data-type="curl" href="#"><b>%s</b></a>%s',
3696 $this->get_text_inline( 'I don\'t know what is cURL or how to install it, help me!', 'curl-missing-no-clue-title' ),
3697 ' - ' . sprintf(
3698 $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' ),
3699 '<a href="mailto:' . $admin_email . '">' . $admin_email . '</a>'
3700 )
3701 ),
3702 sprintf(
3703 '<b>%s</b> - %s',
3704 $sysadmin_title,
3705 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 ) ) )
3706 ),
3707 sprintf(
3708 '<a href="%s"><b>%s</b></a> - %s',
3709 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 ),
3710 $deactivate_plugin_title,
3711 $deactivate_plugin_desc
3712 )
3713 )
3714 );
3715 break;
3716 case 'cloudflare_ddos_protection':
3717 $message = sprintf(
3718 $x_requires_access_to_api . ' ' .
3719 $this->esc_html_inline( 'From unknown reason, CloudFlare, the firewall we use, blocks the connection.', 'cloudflare-blocks-connection-message' ) . ' ' .
3720 $happy_to_resolve_issue_asap .
3721 ' %s',
3722 '<b>' . $this->get_plugin_name() . '</b>',
3723 sprintf(
3724 '<ol id="fs_firewall_issue_options"><li>%s</li><li>%s</li><li>%s</li></ol>',
3725 sprintf(
3726 '<a class="fs-resolve" data-type="cloudflare" href="#"><b>%s</b></a>%s',
3727 $fix_issue_title,
3728 ' - ' . sprintf(
3729 $fix_issue_desc,
3730 '<a href="mailto:' . $admin_email . '">' . $admin_email . '</a>'
3731 )
3732 ),
3733 sprintf(
3734 '<a href="%s" target="_blank"><b>%s</b></a> - %s',
3735 sprintf( 'https://wordpress.org/plugins/%s/download/', $this->_slug ),
3736 $install_previous_title,
3737 $install_previous_desc
3738 ),
3739 sprintf(
3740 '<a href="%s"><b>%s</b></a> - %s',
3741 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 ),
3742 $deactivate_plugin_title,
3743 $deactivate_plugin_desc
3744 )
3745 )
3746 );
3747 break;
3748 case 'squid_cache_block':
3749 $message = sprintf(
3750 $x_requires_access_to_api . ' ' .
3751 $this->esc_html_inline( 'It looks like your server is using Squid ACL (access control lists), which blocks the connection.', 'squid-blocks-connection-message' ) .
3752 ' %s',
3753 '<b>' . $this->get_plugin_name() . '</b>',
3754 sprintf(
3755 '<ol id="fs_firewall_issue_options"><li>%s</li><li>%s</li><li>%s</li></ol>',
3756 sprintf(
3757 '<a class="fs-resolve" data-type="squid" href="#"><b>%s</b></a> - %s',
3758 $this->esc_html_inline( 'I don\'t know what is Squid or ACL, help me!', 'squid-no-clue-title' ),
3759 sprintf(
3760 $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' ),
3761 '<a href="mailto:' . $admin_email . '">' . $admin_email . '</a>'
3762 )
3763 ),
3764 sprintf(
3765 '<b>%s</b> - %s',
3766 $sysadmin_title,
3767 sprintf(
3768 $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' ),
3769 // We use a filter since the plugin might require additional API connectivity.
3770 '<b>' . implode( ', ', $this->apply_filters( 'api_domains', array(
3771 'api.freemius.com',
3772 'wp.freemius.com'
3773 ) ) ) . '</b>',
3774 $this->_module_type
3775 )
3776 ),
3777 sprintf(
3778 '<a href="%s"><b>%s</b></a> - %s',
3779 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 ),
3780 $deactivate_plugin_title,
3781 $deactivate_plugin_desc
3782 )
3783 )
3784 );
3785 break;
3786 // default:
3787 // $message = $this->get_text_inline( 'connectivity-test-fails-message' );
3788 // break;
3789 }
3790 }
3791
3792 $message_id = 'failed_connect_api';
3793 $type = 'error';
3794
3795 $connectivity_test_fails_message = $this->esc_html_inline( 'From unknown reason, the API connectivity test failed.', 'connectivity-test-fails-message' );
3796
3797 if ( false === $message ) {
3798 if ( $is_first_failure ) {
3799 // First attempt failed.
3800 $message = sprintf(
3801 $x_requires_access_to_api . ' ' .
3802 $connectivity_test_fails_message . ' ' .
3803 $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>' .
3804 '%s',
3805 '<b>' . $this->get_plugin_name() . '</b>',
3806 sprintf(
3807 '<div id="fs_firewall_issue_options">%s %s</div>',
3808 sprintf(
3809 '<a class="button button-primary fs-resolve" data-type="retry_ping" href="#">%s</a>',
3810 $this->get_text_inline( 'Yes - do your thing', 'yes-do-your-thing' )
3811 ),
3812 sprintf(
3813 '<a href="%s" class="button">%s</a>',
3814 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 ),
3815 $this->get_text_inline( 'No - just deactivate', 'no-deactivate' )
3816 )
3817 )
3818 );
3819
3820 $message_id = 'failed_connect_api_first';
3821 $type = 'promotion';
3822 } else {
3823 // Second connectivity attempt failed.
3824 $message = sprintf(
3825 $x_requires_access_to_api . ' ' .
3826 $connectivity_test_fails_message . ' ' .
3827 $happy_to_resolve_issue_asap .
3828 ' %s',
3829 '<b>' . $this->get_plugin_name() . '</b>',
3830 sprintf(
3831 '<ol id="fs_firewall_issue_options"><li>%s</li><li>%s</li><li>%s</li></ol>',
3832 sprintf(
3833 '<a class="fs-resolve" data-type="general" href="#"><b>%s</b></a>%s',
3834 $fix_issue_title,
3835 ' - ' . sprintf(
3836 $fix_issue_desc,
3837 '<a href="mailto:' . $admin_email . '">' . $admin_email . '</a>'
3838 )
3839 ),
3840 sprintf(
3841 '<a href="%s" target="_blank"><b>%s</b></a> - %s',
3842 sprintf( 'https://wordpress.org/plugins/%s/download/', $this->_slug ),
3843 $install_previous_title,
3844 $install_previous_desc
3845 ),
3846 sprintf(
3847 '<a href="%s"><b>%s</b></a> - %s',
3848 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 ),
3849 $deactivate_plugin_title,
3850 $deactivate_plugin_desc
3851 )
3852 )
3853 );
3854 }
3855 }
3856
3857 $this->_admin_notices->add_sticky(
3858 $message,
3859 $message_id,
3860 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
3861 $type
3862 );
3863 }
3864
3865 /**
3866 * Handle user request to resolve connectivity issue.
3867 * This method will send an email to Freemius API technical staff for resolution.
3868 * The email will contain server's info and installed plugins (might be caching issue).
3869 *
3870 * @author Vova Feldman (@svovaf)
3871 * @since 1.0.9
3872 */
3873 function _email_about_firewall_issue() {
3874 $this->_admin_notices->remove_sticky( 'failed_connect_api' );
3875
3876 $pong = $this->ping();
3877
3878 $is_connected = $this->get_api_plugin_scope()->is_valid_ping( $pong );
3879
3880 if ( $is_connected ) {
3881 FS_GDPR_Manager::instance()->store_is_required( $pong->is_gdpr_required );
3882
3883 $this->store_connectivity_info( $pong, $is_connected );
3884
3885 echo $this->get_after_plugin_activation_redirect_url();
3886 exit;
3887 }
3888
3889 $current_user = self::_get_current_wp_user();
3890 $admin_email = $current_user->user_email;
3891
3892 $error_type = fs_request_get( 'error_type', 'general' );
3893
3894 switch ( $error_type ) {
3895 case 'squid':
3896 $title = 'Squid ACL Blocking Issue';
3897 break;
3898 case 'cloudflare':
3899 $title = 'CloudFlare Blocking Issue';
3900 break;
3901 default:
3902 $title = 'API Connectivity Issue';
3903 break;
3904 }
3905
3906 $custom_email_sections = array();
3907
3908 // Add 'API Error' custom email section.
3909 $custom_email_sections['api_error'] = array(
3910 'title' => 'API Error',
3911 'rows' => array(
3912 'ping' => array(
3913 'API Error',
3914 is_string( $pong ) ? htmlentities( $pong ) : json_encode( $pong )
3915 ),
3916 )
3917 );
3918
3919 // Send email with technical details to resolve API connectivity issues.
3920 $this->send_email(
3921 'api@freemius.com', // recipient
3922 $title . ' [' . $this->get_plugin_name() . ']', // subject
3923 $custom_email_sections,
3924 array( "Reply-To: $admin_email <$admin_email>" ) // headers
3925 );
3926
3927 $this->_admin_notices->add_sticky(
3928 sprintf(
3929 $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' ),
3930 '<a href="mailto:' . $admin_email . '">' . $admin_email . '</a>'
3931 ),
3932 'server_details_sent'
3933 );
3934
3935 // Action was taken, tell that API connectivity troubleshooting should be off now.
3936
3937 echo "1";
3938 exit;
3939 }
3940
3941 /**
3942 * Handle connectivity test retry approved by the user.
3943 *
3944 * @author Vova Feldman (@svovaf)
3945 * @since 1.1.7.4
3946 */
3947 function _retry_connectivity_test() {
3948 $this->_admin_notices->remove_sticky( 'failed_connect_api_first' );
3949
3950 $pong = $this->ping();
3951
3952 $is_connected = $this->get_api_plugin_scope()->is_valid_ping( $pong );
3953
3954 if ( $is_connected ) {
3955 FS_GDPR_Manager::instance()->store_is_required( $pong->is_gdpr_required );
3956
3957 $this->store_connectivity_info( $pong, $is_connected );
3958
3959 echo $this->get_after_plugin_activation_redirect_url();
3960 } else {
3961 // Add connectivity issue message after 2nd failed attempt.
3962 $this->_add_connectivity_issue_message( $pong, false );
3963
3964 echo "1";
3965 }
3966
3967 exit;
3968 }
3969
3970 static function _add_firewall_issues_javascript() {
3971 $params = array();
3972 fs_require_once_template( 'firewall-issues-js.php', $params );
3973 }
3974
3975 #endregion
3976
3977 #----------------------------------------------------------------------------------
3978 #region Email
3979 #----------------------------------------------------------------------------------
3980
3981 /**
3982 * Generates and sends an HTML email with customizable sections.
3983 *
3984 * @author Leo Fajardo (@leorw)
3985 * @since 1.1.2
3986 *
3987 * @param string $to_address
3988 * @param string $subject
3989 * @param array $sections
3990 * @param array $headers
3991 *
3992 * @return bool Whether the email contents were sent successfully.
3993 */
3994 private function send_email(
3995 $to_address,
3996 $subject,
3997 $sections = array(),
3998 $headers = array()
3999 ) {
4000 $default_sections = $this->get_email_sections();
4001
4002 // Insert new sections or replace the default email sections.
4003 if ( is_array( $sections ) && ! empty( $sections ) ) {
4004 foreach ( $sections as $section_id => $custom_section ) {
4005 if ( ! isset( $default_sections[ $section_id ] ) ) {
4006 // If the section does not exist, add it.
4007 $default_sections[ $section_id ] = $custom_section;
4008 } else {
4009 // If the section already exists, override it.
4010 $current_section = $default_sections[ $section_id ];
4011
4012 // Replace the current section's title if a custom section title exists.
4013 if ( isset( $custom_section['title'] ) ) {
4014 $current_section['title'] = $custom_section['title'];
4015 }
4016
4017 // Insert new rows under the current section or replace the default rows.
4018 if ( isset( $custom_section['rows'] ) && is_array( $custom_section['rows'] ) && ! empty( $custom_section['rows'] ) ) {
4019 foreach ( $custom_section['rows'] as $row_id => $row ) {
4020 $current_section['rows'][ $row_id ] = $row;
4021 }
4022 }
4023
4024 $default_sections[ $section_id ] = $current_section;
4025 }
4026 }
4027 }
4028
4029 $vars = array( 'sections' => $default_sections );
4030 $message = fs_get_template( 'email.php', $vars );
4031
4032 // Set the type of email to HTML.
4033 $headers[] = 'Content-type: text/html; charset=UTF-8';
4034
4035 $header_string = implode( "\r\n", $headers );
4036
4037 return wp_mail(
4038 $to_address,
4039 $subject,
4040 $message,
4041 $header_string
4042 );
4043 }
4044
4045 /**
4046 * Generates the data for the sections of the email content.
4047 *
4048 * @author Leo Fajardo (@leorw)
4049 * @since 1.1.2
4050 *
4051 * @return array
4052 */
4053 private function get_email_sections() {
4054 // Retrieve the current user's information so that we can get the user's email, first name, and last name below.
4055 $current_user = self::_get_current_wp_user();
4056
4057 // Retrieve the cURL version information so that we can get the version number below.
4058 $curl_version_information = curl_version();
4059
4060 $active_plugin = self::get_active_plugins();
4061
4062 // Generate the list of active plugins separated by new line.
4063 $active_plugin_string = '';
4064 foreach ( $active_plugin as $plugin ) {
4065 $active_plugin_string .= sprintf(
4066 '<a href="%s">%s</a> [v%s]<br>',
4067 $plugin['PluginURI'],
4068 $plugin['Name'],
4069 $plugin['Version']
4070 );
4071 }
4072
4073 $server_ip = WP_FS__REMOTE_ADDR;
4074
4075 // Add PHP info for deeper investigation.
4076 ob_start();
4077 phpinfo();
4078 $php_info = ob_get_clean();
4079
4080 $api_domain = substr( FS_API__ADDRESS, strpos( FS_API__ADDRESS, ':' ) + 3 );
4081
4082 // Generate the default email sections.
4083 $sections = array(
4084 'sdk' => array(
4085 'title' => 'SDK',
4086 'rows' => array(
4087 'fs_version' => array( 'FS Version', $this->version ),
4088 'curl_version' => array( 'cURL Version', $curl_version_information['version'] )
4089 )
4090 ),
4091 'plugin' => array(
4092 'title' => ucfirst( $this->get_module_type() ),
4093 'rows' => array(
4094 'name' => array( 'Name', $this->get_plugin_name() ),
4095 'version' => array( 'Version', $this->get_plugin_version() )
4096 )
4097 ),
4098 'api' => array(
4099 'title' => 'API Subdomain',
4100 'rows' => array(
4101 'dns' => array(
4102 'DNS_CNAME',
4103 function_exists( 'dns_get_record' ) ?
4104 var_export( dns_get_record( $api_domain, DNS_CNAME ), true ) :
4105 'dns_get_record() disabled/blocked'
4106 ),
4107 'ip' => array(
4108 'IP',
4109 function_exists( 'gethostbyname' ) ?
4110 gethostbyname( $api_domain ) :
4111 'gethostbyname() disabled/blocked'
4112 ),
4113 ),
4114 ),
4115 'site' => array(
4116 'title' => 'Site',
4117 'rows' => array(
4118 'unique_id' => array( 'Unique ID', $this->get_anonymous_id() ),
4119 'address' => array( 'Address', site_url() ),
4120 'host' => array(
4121 'HTTP_HOST',
4122 ( ! empty( $_SERVER['HTTP_HOST'] ) ? $_SERVER['HTTP_HOST'] : '' )
4123 ),
4124 'hosting' => array(
4125 'Hosting Company' => fs_request_has( 'hosting_company' ) ?
4126 fs_request_get( 'hosting_company' ) :
4127 'Unknown',
4128 ),
4129 'server_addr' => array(
4130 'SERVER_ADDR',
4131 '<a href="http://www.projecthoneypot.org/ip_' . $server_ip . '">' . $server_ip . '</a>'
4132 )
4133 )
4134 ),
4135 'user' => array(
4136 'title' => 'User',
4137 'rows' => array(
4138 'email' => array( 'Email', $current_user->user_email ),
4139 'first' => array( 'First', $current_user->user_firstname ),
4140 'last' => array( 'Last', $current_user->user_lastname )
4141 )
4142 ),
4143 'plugins' => array(
4144 'title' => 'Plugins',
4145 'rows' => array(
4146 'active_plugins' => array( 'Active Plugins', $active_plugin_string )
4147 )
4148 ),
4149 'php_info' => array(
4150 'title' => 'PHP Info',
4151 'rows' => array(
4152 'info' => array( $php_info )
4153 ),
4154 )
4155 );
4156
4157 // Allow the sections to be modified by other code.
4158 $sections = $this->apply_filters( 'email_template_sections', $sections );
4159
4160 return $sections;
4161 }
4162
4163 #endregion
4164
4165 #----------------------------------------------------------------------------------
4166 #region Initialization
4167 #----------------------------------------------------------------------------------
4168
4169 /**
4170 * Init plugin's Freemius instance.
4171 *
4172 * @author Vova Feldman (@svovaf)
4173 * @since 1.0.1
4174 *
4175 * @param number $id
4176 * @param string $public_key
4177 * @param bool $is_live
4178 * @param bool $is_premium
4179 */
4180 function init( $id, $public_key, $is_live = true, $is_premium = true ) {
4181 $this->_logger->entrance();
4182
4183 $this->dynamic_init( array(
4184 'id' => $id,
4185 'public_key' => $public_key,
4186 'is_live' => $is_live,
4187 'is_premium' => $is_premium,
4188 ) );
4189 }
4190
4191 /**
4192 * Dynamic initiator, originally created to support initiation
4193 * with parent_id for add-ons.
4194 *
4195 * @author Vova Feldman (@svovaf)
4196 * @since 1.0.6
4197 *
4198 * @param array $plugin_info
4199 *
4200 * @throws Freemius_Exception
4201 */
4202 function dynamic_init( array $plugin_info ) {
4203 $this->_logger->entrance();
4204
4205 $this->parse_settings( $plugin_info );
4206
4207 if ( is_admin() && $this->is_theme() && $this->is_premium() && ! $this->has_active_valid_license() ) {
4208 $this->add_ajax_action(
4209 'delete_theme_update_data',
4210 array( &$this, '_delete_theme_update_data_action' )
4211 );
4212 }
4213
4214 if ( ! self::is_ajax() ) {
4215 if ( ! $this->is_addon() || $this->is_only_premium() ) {
4216 add_action(
4217 ( $this->_is_network_active && fs_is_network_admin() ? 'network_' : '' ) . 'admin_menu',
4218 array( &$this, '_prepare_admin_menu' ),
4219 WP_FS__LOWEST_PRIORITY
4220 );
4221 }
4222 }
4223
4224 if ( $this->should_stop_execution() ) {
4225 return;
4226 }
4227
4228 if ( ! $this->is_registered() ) {
4229 if ( $this->is_anonymous() ) {
4230 // If user skipped, no need to test connectivity.
4231 $this->_has_api_connection = true;
4232 $this->_is_on = true;
4233 } else {
4234 if ( ! $this->has_api_connectivity() ) {
4235 if ( $this->_admin_notices->has_sticky( 'failed_connect_api_first' ) ||
4236 $this->_admin_notices->has_sticky( 'failed_connect_api' )
4237 ) {
4238 if ( ! $this->_enable_anonymous || $this->is_premium() ) {
4239 // If anonymous mode is disabled, add firewall admin-notice message.
4240 add_action( 'admin_footer', array( 'Freemius', '_add_firewall_issues_javascript' ) );
4241
4242 $ajax_action_suffix = $this->_slug . ( $this->is_theme() ? ':theme' : '' );
4243 add_action( "wp_ajax_fs_resolve_firewall_issues_{$ajax_action_suffix}", array(
4244 &$this,
4245 '_email_about_firewall_issue'
4246 ) );
4247
4248 add_action( "wp_ajax_fs_retry_connectivity_test_{$ajax_action_suffix}", array(
4249 &$this,
4250 '_retry_connectivity_test'
4251 ) );
4252
4253 /**
4254 * 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.
4255 *
4256 * @author Vova Feldman (@svovaf)
4257 * @since 2.0.0
4258 */
4259 /*$this->add_ajax_action( 'resolve_firewall_issues', array(
4260 &$this,
4261 '_email_about_firewall_issue'
4262 ) );
4263
4264 $this->add_ajax_action( 'retry_connectivity_test', array(
4265 &$this,
4266 '_retry_connectivity_test'
4267 ) );*/
4268 }
4269 }
4270
4271 return;
4272 } else {
4273 $this->_admin_notices->remove_sticky( array(
4274 'failed_connect_api_first',
4275 'failed_connect_api',
4276 ) );
4277
4278 if ( $this->_anonymous_mode ) {
4279 // Simulate anonymous mode.
4280 $this->_is_anonymous = true;
4281 }
4282 }
4283 }
4284 }
4285
4286 /**
4287 * This should be executed even if Freemius is off for the core module,
4288 * otherwise, the add-ons dialogbox won't work properly. This is esepcially
4289 * relevant when the developer decided to turn FS off for existing users.
4290 *
4291 * @author Vova Feldman (@svovaf)
4292 */
4293 if ( $this->is_user_in_admin() &&
4294 'plugin-information' === fs_request_get( 'tab', false ) &&
4295 $this->should_use_freemius_updater_and_dialog() &&
4296 (
4297 ( $this->is_addon() && $this->get_slug() == fs_request_get( 'plugin', false ) ) ||
4298 ( $this->has_addons() && $this->get_id() == fs_request_get( 'parent_plugin_id', false ) )
4299 )
4300 ) {
4301 require_once WP_FS__DIR_INCLUDES . '/fs-plugin-info-dialog.php';
4302
4303 new FS_Plugin_Info_Dialog( $this->is_addon() ? $this->get_parent_instance() : $this );
4304 }
4305
4306 // Check if Freemius is on for the current plugin.
4307 // This MUST be executed after all the plugin variables has been loaded.
4308 if ( ! $this->is_registered() && ! $this->is_on() ) {
4309 return;
4310 }
4311
4312 if ( $this->has_api_connectivity() ) {
4313 if ( self::is_cron() ) {
4314 $this->hook_callback_to_sync_cron();
4315 } else if ( $this->is_user_in_admin() ) {
4316 /**
4317 * Schedule daily data sync cron if:
4318 *
4319 * 1. User opted-in (for tracking).
4320 * 2. If skipped, but later upgraded (opted-in via upgrade).
4321 *
4322 * @author Vova Feldman (@svovaf)
4323 * @since 1.1.7.3
4324 *
4325 */
4326 if ( $this->is_registered() ) {
4327 if ( ! $this->is_sync_cron_on() && $this->is_tracking_allowed() ) {
4328 $this->schedule_sync_cron();
4329 }
4330 }
4331
4332 /**
4333 * Check if requested for manual blocking background sync.
4334 */
4335 if ( fs_request_has( 'background_sync' ) ) {
4336 $this->run_manual_sync();
4337 }
4338 }
4339 }
4340
4341 if ( $this->is_registered() ) {
4342 $this->hook_callback_to_install_sync();
4343 }
4344
4345 if ( $this->is_addon() ) {
4346 if ( $this->is_parent_plugin_installed() ) {
4347 // Link to parent FS.
4348 $this->_parent = self::get_instance_by_id( $this->_plugin->parent_plugin_id );
4349
4350 // Get parent plugin reference.
4351 $this->_parent_plugin = $this->_parent->get_plugin();
4352 }
4353 }
4354
4355 if ( $this->is_user_in_admin() ) {
4356 if ( $this->is_addon() ) {
4357 if ( ! $this->is_parent_plugin_installed() ) {
4358 $parent_name = $this->get_option( $plugin_info, 'parent_name', null );
4359
4360 if ( isset( $plugin_info['parent'] ) ) {
4361 $parent_name = $this->get_option( $plugin_info['parent'], 'name', null );
4362 }
4363
4364 $this->_admin_notices->add(
4365 ( ! empty( $parent_name ) ?
4366 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 ) :
4367 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() )
4368 ),
4369 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
4370 'error'
4371 );
4372
4373 return;
4374 } else {
4375 if ( $this->_parent->is_registered() && ! $this->is_registered() ) {
4376 // If parent plugin activated, automatically install add-on for the user.
4377 $this->_activate_addon_account( $this->_parent );
4378 } else if ( ! $this->_parent->is_registered() && $this->is_registered() ) {
4379 // If add-on activated and parent not, automatically install parent for the user.
4380 $this->activate_parent_account( $this->_parent );
4381 }
4382
4383 // @todo This should be only executed on activation. It should be migrated to register_activation_hook() together with other activation related logic.
4384 if ( $this->is_premium() ) {
4385 // Remove add-on download admin-notice.
4386 $this->_parent->_admin_notices->remove_sticky( array(
4387 'addon_plan_upgraded_' . $this->_slug,
4388 'no_addon_license_' . $this->_slug,
4389 ) );
4390 }
4391
4392 // $this->deactivate_premium_only_addon_without_license();
4393 }
4394 }
4395
4396 add_action( 'admin_init', array( &$this, '_admin_init_action' ) );
4397
4398 // if ( $this->is_registered() ||
4399 // $this->is_anonymous() ||
4400 // $this->is_pending_activation()
4401 // ) {
4402 // $this->_init_admin();
4403 // }
4404 }
4405
4406 /**
4407 * Should be called outside `$this->is_user_in_admin()` scope
4408 * because the updater has some logic that needs to be executed
4409 * during AJAX calls.
4410 *
4411 * Currently we need to hook to the `http_request_host_is_external` filter.
4412 * In the future, there might be additional logic added.
4413 *
4414 * @author Vova Feldman
4415 * @since 1.2.1.6
4416 */
4417 if (
4418 $this->should_use_freemius_updater_and_dialog() &&
4419 (
4420 $this->is_premium() ||
4421 /**
4422 * If not premium but the premium version is installed, also instantiate the updater so that the
4423 * plugin information dialog of the premium version will have the information from the server.
4424 *
4425 * @author Leo Fajardo (@leorw)
4426 * @since 2.2.3
4427 */
4428 ( file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $this->premium_plugin_basename() ) ) )
4429 ) &&
4430 $this->has_release_on_freemius()
4431 ) {
4432 FS_Plugin_Updater::instance( $this );
4433 }
4434
4435 $this->do_action( 'initiated' );
4436
4437 if ( $this->_storage->prev_is_premium !== $this->_plugin->is_premium ) {
4438 if ( isset( $this->_storage->prev_is_premium ) ) {
4439 $this->apply_filters(
4440 'after_code_type_change',
4441 // New code type.
4442 $this->_plugin->is_premium
4443 );
4444 } else {
4445 // Set for code type for the first time.
4446 $this->_storage->prev_is_premium = $this->_plugin->is_premium;
4447 }
4448 }
4449
4450 if ( ! $this->is_addon() ) {
4451 if ( $this->is_registered() ) {
4452 // Fix for upgrade from versions < 1.0.9.
4453 if ( ! isset( $this->_storage->activation_timestamp ) ) {
4454 $this->_storage->activation_timestamp = WP_FS__SCRIPT_START_TIME;
4455 }
4456
4457 $this->do_action( 'after_init_plugin_registered' );
4458 } else if ( $this->is_anonymous() ) {
4459 $this->do_action( 'after_init_plugin_anonymous' );
4460 } else if ( $this->is_pending_activation() ) {
4461 $this->do_action( 'after_init_plugin_pending_activations' );
4462 }
4463 } else {
4464 if ( $this->is_registered() ) {
4465 $this->do_action( 'after_init_addon_registered' );
4466 } else if ( $this->is_anonymous() ) {
4467 $this->do_action( 'after_init_addon_anonymous' );
4468 } else if ( $this->is_pending_activation() ) {
4469 $this->do_action( 'after_init_addon_pending_activations' );
4470 }
4471 }
4472 }
4473
4474 /**
4475 * @author Leo Fajardo (@leorw)
4476 * @since 2.2.3
4477 *
4478 * @return bool
4479 */
4480 private function should_use_freemius_updater_and_dialog() {
4481 return (
4482 /**
4483 * Unless the `fs_allow_updater_and_dialog` URL param exists and its value is `true`, disallow updater
4484 * and dialog on the "Add Plugins" admin page (/plugin-install.php) so that they won't interfere with
4485 * the .org plugins' functionalities on that page (e.g. installation and viewing plugin details from
4486 * .org).
4487 */
4488 ( ! self::is_plugin_install_page() || true === fs_request_get_bool( 'fs_allow_updater_and_dialog' ) ) &&
4489 // Disallow updater and dialog when installing a plugin, otherwise .org "add-on" plugins will be affected.
4490 ( 'install-plugin' !== fs_request_get( 'action' ) )
4491 );
4492 }
4493
4494 /**
4495 * @author Leo Fajardo (@leorw)
4496 *
4497 * @since 1.2.1.5
4498 */
4499 function _stop_tracking_callback() {
4500 $this->_logger->entrance();
4501
4502 $this->check_ajax_referer( 'stop_tracking' );
4503
4504 $result = $this->stop_tracking( fs_is_network_admin() );
4505
4506 if ( true === $result ) {
4507 self::shoot_ajax_success();
4508 }
4509
4510 $this->_logger->api_error( $result );
4511
4512 self::shoot_ajax_failure(
4513 sprintf( $this->get_text_inline( 'Unexpected API error. Please contact the %s\'s author with the following error.', 'unexpected-api-error' ), $this->_module_type ) .
4514 ( $this->is_api_error( $result ) && isset( $result->error ) ?
4515 $result->error->message :
4516 var_export( $result, true ) )
4517 );
4518 }
4519
4520 /**
4521 * @author Leo Fajardo (@leorw)
4522 * @since 1.2.1.5
4523 */
4524 function _allow_tracking_callback() {
4525 $this->_logger->entrance();
4526
4527 $this->check_ajax_referer( 'allow_tracking' );
4528
4529 $result = $this->allow_tracking( fs_is_network_admin() );
4530
4531 if ( true === $result ) {
4532 self::shoot_ajax_success();
4533 }
4534
4535 $this->_logger->api_error( $result );
4536
4537 self::shoot_ajax_failure(
4538 sprintf( $this->get_text_inline( 'Unexpected API error. Please contact the %s\'s author with the following error.', 'unexpected-api-error' ), $this->_module_type ) .
4539 ( $this->is_api_error( $result ) && isset( $result->error ) ?
4540 $result->error->message :
4541 var_export( $result, true ) )
4542 );
4543 }
4544
4545 /**
4546 * Opt-out from usage tracking.
4547 *
4548 * Note: This will not delete the account information but will stop all tracking.
4549 *
4550 * Returns:
4551 * 1. FALSE - If the user never opted-in.
4552 * 2. TRUE - If successfully opted-out.
4553 * 3. object - API result on failure.
4554 *
4555 * @author Leo Fajardo (@leorw)
4556 * @since 1.2.1.5
4557 *
4558 * @return bool|object
4559 */
4560 function stop_site_tracking() {
4561 $this->_logger->entrance();
4562
4563 if ( ! $this->is_registered() ) {
4564 // User never opted-in.
4565 return false;
4566 }
4567
4568 if ( $this->is_tracking_prohibited() ) {
4569 // Already disconnected.
4570 return true;
4571 }
4572
4573 // Send update to FS.
4574 $result = $this->get_api_site_scope()->call( '/?fields=is_disconnected', 'put', array(
4575 'is_disconnected' => true
4576 ) );
4577
4578 if ( ! $this->is_api_result_entity( $result ) ||
4579 ! isset( $result->is_disconnected ) ||
4580 ! $result->is_disconnected
4581 ) {
4582 $this->_logger->api_error( $result );
4583
4584 return $result;
4585 }
4586
4587 $this->_site->is_disconnected = $result->is_disconnected;
4588 $this->_store_site();
4589
4590 $this->clear_sync_cron();
4591
4592 // Successfully disconnected.
4593 return true;
4594 }
4595
4596 /**
4597 * Opt-out network from usage tracking.
4598 *
4599 * Note: This will not delete the account information but will stop all tracking.
4600 *
4601 * Returns:
4602 * 1. FALSE - If the user never opted-in.
4603 * 2. TRUE - If successfully opted-out.
4604 * 3. object - API result on failure.
4605 *
4606 * @author Leo Fajardo (@leorw)
4607 * @since 1.2.1.5
4608 *
4609 * @return bool|object
4610 */
4611 function stop_network_tracking() {
4612 $this->_logger->entrance();
4613
4614 if ( ! $this->is_registered() ) {
4615 // User never opted-in.
4616 return false;
4617 }
4618
4619 $install_id_2_blog_id = array();
4620 $installs_map = $this->get_blog_install_map();
4621
4622 $opt_out_all = true;
4623
4624 $params = array();
4625 foreach ( $installs_map as $blog_id => $install ) {
4626 if ( $install->is_tracking_prohibited() ) {
4627 // Already opted-out.
4628 continue;
4629 }
4630
4631 if ( $this->is_site_delegated_connection( $blog_id ) ) {
4632 // Opt-out only from non-delegated installs.
4633 $opt_out_all = false;
4634 continue;
4635 }
4636
4637 $params[] = array( 'id' => $install->id );
4638
4639 $install_id_2_blog_id[ $install->id ] = $blog_id;
4640 }
4641
4642 if ( empty( $install_id_2_blog_id ) ) {
4643 return true;
4644 }
4645
4646 $params[] = array( 'is_disconnected' => true );
4647
4648 // Send update to FS.
4649 $result = $this->get_current_or_network_user_api_scope()->call( "/plugins/{$this->_module_id}/installs.json", 'put', $params );
4650
4651 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
4652 $this->_logger->api_error( $result );
4653
4654 return $result;
4655 }
4656
4657 foreach ( $result->installs as $r_install ) {
4658 $blog_id = $install_id_2_blog_id[ $r_install->id ];
4659 $install = $installs_map[ $blog_id ];
4660 $install->is_disconnected = $r_install->is_disconnected;
4661 $this->_store_site( true, $blog_id, $install );
4662 }
4663
4664 $this->clear_sync_cron( $opt_out_all );
4665
4666 // Successfully disconnected.
4667 return true;
4668 }
4669
4670 /**
4671 * Opt-out from usage tracking.
4672 *
4673 * Note: This will not delete the account information but will stop all tracking.
4674 *
4675 * Returns:
4676 * 1. FALSE - If the user never opted-in.
4677 * 2. TRUE - If successfully opted-out.
4678 * 3. object - API result on failure.
4679 *
4680 * @author Leo Fajardo (@leorw)
4681 * @since 1.2.1.5
4682 *
4683 * @param bool $is_network_action
4684 *
4685 * @return bool|object
4686 */
4687 function stop_tracking( $is_network_action = false ) {
4688 $this->_logger->entrance();
4689
4690 return $is_network_action ?
4691 $this->stop_network_tracking() :
4692 $this->stop_site_tracking();
4693 }
4694
4695 /**
4696 * Opt-in back into usage tracking.
4697 *
4698 * Note: This will only work if the user opted-in previously.
4699 *
4700 * Returns:
4701 * 1. FALSE - If the user never opted-in.
4702 * 2. TRUE - If successfully opted-in back to usage tracking.
4703 * 3. object - API result on failure.
4704 *
4705 * @author Leo Fajardo (@leorw)
4706 * @since 1.2.1.5
4707 *
4708 * @return bool|object
4709 */
4710 function allow_site_tracking() {
4711 $this->_logger->entrance();
4712
4713 if ( ! $this->is_registered() ) {
4714 // User never opted-in.
4715 return false;
4716 }
4717
4718 if ( $this->is_tracking_allowed() ) {
4719 // Tracking already allowed.
4720 return true;
4721 }
4722
4723 $result = $this->get_api_site_scope()->call( '/?is_disconnected', 'put', array(
4724 'is_disconnected' => false
4725 ) );
4726
4727 if ( ! $this->is_api_result_entity( $result ) ||
4728 ! isset( $result->is_disconnected ) ||
4729 $result->is_disconnected
4730 ) {
4731 $this->_logger->api_error( $result );
4732
4733 return $result;
4734 }
4735
4736 $this->_site->is_disconnected = $result->is_disconnected;
4737 $this->_store_site();
4738
4739 $this->schedule_sync_cron();
4740
4741 // Successfully reconnected.
4742 return true;
4743 }
4744
4745 /**
4746 * Opt-in network back into usage tracking.
4747 *
4748 * Note: This will only work if the user opted-in previously.
4749 *
4750 * Returns:
4751 * 1. FALSE - If the user never opted-in.
4752 * 2. TRUE - If successfully opted-in back to usage tracking.
4753 * 3. object - API result on failure.
4754 *
4755 * @author Leo Fajardo (@leorw)
4756 * @since 1.2.1.5
4757 *
4758 * @return bool|object
4759 */
4760 function allow_network_tracking() {
4761 $this->_logger->entrance();
4762
4763 if ( ! $this->is_registered() ) {
4764 // User never opted-in.
4765 return false;
4766 }
4767
4768 $install_id_2_blog_id = array();
4769 $installs_map = $this->get_blog_install_map();
4770
4771 $params = array();
4772 foreach ( $installs_map as $blog_id => $install ) {
4773 if ( $install->is_tracking_allowed() ) {
4774 continue;
4775 }
4776
4777 $params[] = array( 'id' => $install->id );
4778
4779 $install_id_2_blog_id[ $install->id ] = $blog_id;
4780 }
4781
4782 if ( empty( $install_id_2_blog_id ) ) {
4783 return true;
4784 }
4785
4786 $params[] = array( 'is_disconnected' => false );
4787
4788 // Send update to FS.
4789 $result = $this->get_current_or_network_user_api_scope()->call( "/plugins/{$this->_module_id}/installs.json", 'put', $params );
4790
4791
4792 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
4793 $this->_logger->api_error( $result );
4794
4795 return $result;
4796 }
4797
4798 foreach ( $result->installs as $r_install ) {
4799 $blog_id = $install_id_2_blog_id[ $r_install->id ];
4800 $install = $installs_map[ $blog_id ];
4801 $install->is_disconnected = $r_install->is_disconnected;
4802 $this->_store_site( true, $blog_id, $install );
4803 }
4804
4805 $this->schedule_sync_cron();
4806
4807 // Successfully reconnected.
4808 return true;
4809 }
4810
4811 /**
4812 * Opt-in back into usage tracking.
4813 *
4814 * Note: This will only work if the user opted-in previously.
4815 *
4816 * Returns:
4817 * 1. FALSE - If the user never opted-in.
4818 * 2. TRUE - If successfully opted-in back to usage tracking.
4819 * 3. object - API result on failure.
4820 *
4821 * @author Leo Fajardo (@leorw)
4822 * @since 1.2.1.5
4823 *
4824 * @param bool $is_network_action
4825 *
4826 * @return bool|object
4827 */
4828 function allow_tracking( $is_network_action = false ) {
4829 $this->_logger->entrance();
4830
4831 return $is_network_action ?
4832 $this->allow_network_tracking() :
4833 $this->allow_site_tracking();
4834 }
4835
4836 /**
4837 * If user opted-in and later disabled usage-tracking,
4838 * re-allow tracking for licensing and updates.
4839 *
4840 * @author Leo Fajardo (@leorw)
4841 * @since 1.2.1.5
4842 *
4843 * @param bool $is_context_single_site
4844 */
4845 private function reconnect_locally( $is_context_single_site = false ) {
4846 $this->_logger->entrance();
4847
4848 if ( ! $this->is_registered() ) {
4849 return;
4850 }
4851
4852 if ( ! fs_is_network_admin() || $is_context_single_site ) {
4853 if ( $this->is_tracking_prohibited() ) {
4854 $this->_site->is_disconnected = false;
4855 $this->_store_site();
4856 }
4857 } else {
4858 $installs_map = $this->get_blog_install_map();
4859 foreach ( $installs_map as $blog_id => $install ) {
4860 /**
4861 * @var FS_Site $install
4862 */
4863 if ( $install->is_tracking_prohibited() ) {
4864 $install->is_disconnected = false;
4865 $this->_store_site( true, $blog_id, $install );
4866 }
4867 }
4868 }
4869 }
4870
4871 /**
4872 * Parse plugin's settings (as defined by the plugin dev).
4873 *
4874 * @author Vova Feldman (@svovaf)
4875 * @since 1.1.7.3
4876 *
4877 * @param array $plugin_info
4878 *
4879 * @throws \Freemius_Exception
4880 */
4881 private function parse_settings( &$plugin_info ) {
4882 $this->_logger->entrance();
4883
4884 $id = $this->get_numeric_option( $plugin_info, 'id', false );
4885 $public_key = $this->get_option( $plugin_info, 'public_key', false );
4886 $secret_key = $this->get_option( $plugin_info, 'secret_key', null );
4887 $parent_id = $this->get_numeric_option( $plugin_info, 'parent_id', null );
4888 $parent_name = $this->get_option( $plugin_info, 'parent_name', null );
4889
4890 /**
4891 * @author Vova Feldman (@svovaf)
4892 * @since 1.1.9 Try to pull secret key from external config.
4893 */
4894 if ( is_null( $secret_key ) && defined( "WP_FS__{$this->_slug}_SECRET_KEY" ) ) {
4895 $secret_key = constant( "WP_FS__{$this->_slug}_SECRET_KEY" );
4896 }
4897
4898 if ( isset( $plugin_info['parent'] ) ) {
4899 $parent_id = $this->get_numeric_option( $plugin_info['parent'], 'id', null );
4900 // $parent_slug = $this->get_option( $plugin_info['parent'], 'slug', null );
4901 // $parent_public_key = $this->get_option( $plugin_info['parent'], 'public_key', null );
4902 // $parent_name = $this->get_option( $plugin_info['parent'], 'name', null );
4903 }
4904
4905 if ( false === $id ) {
4906 throw new Freemius_Exception( array(
4907 'error' => array(
4908 'type' => 'ParameterNotSet',
4909 'message' => 'Plugin id parameter is not set.',
4910 'code' => 'plugin_id_not_set',
4911 'http' => 500,
4912 )
4913 ) );
4914 }
4915 if ( false === $public_key ) {
4916 throw new Freemius_Exception( array(
4917 'error' => array(
4918 'type' => 'ParameterNotSet',
4919 'message' => 'Plugin public_key parameter is not set.',
4920 'code' => 'plugin_public_key_not_set',
4921 'http' => 500,
4922 )
4923 ) );
4924 }
4925
4926 $plugin = ( $this->_plugin instanceof FS_Plugin ) ?
4927 $this->_plugin :
4928 new FS_Plugin();
4929
4930 $premium_suffix = $this->get_option( $plugin_info, 'premium_suffix', '(Premium)' );
4931
4932 $plugin->update( array(
4933 'id' => $id,
4934 'type' => $this->get_option( $plugin_info, 'type', $this->_module_type ),
4935 'public_key' => $public_key,
4936 'slug' => $this->_slug,
4937 'premium_slug' => $this->get_option( $plugin_info, 'premium_slug', "{$this->_slug}-premium" ),
4938 'parent_plugin_id' => $parent_id,
4939 'version' => $this->get_plugin_version(),
4940 'title' => $this->get_plugin_name( $premium_suffix ),
4941 'file' => $this->_plugin_basename,
4942 'is_premium' => $this->get_bool_option( $plugin_info, 'is_premium', true ),
4943 'premium_suffix' => $premium_suffix,
4944 'is_live' => $this->get_bool_option( $plugin_info, 'is_live', true ),
4945 'affiliate_moderation' => $this->get_option( $plugin_info, 'has_affiliation' ),
4946 ) );
4947
4948 if ( $plugin->is_updated() ) {
4949 // Update plugin details.
4950 $this->_plugin = FS_Plugin_Manager::instance( $this->_module_id )->store( $plugin );
4951 }
4952 // Set the secret key after storing the plugin, we don't want to store the key in the storage.
4953 $this->_plugin->secret_key = $secret_key;
4954
4955 if ( ! isset( $plugin_info['menu'] ) ) {
4956 $plugin_info['menu'] = array();
4957
4958 if ( ! empty( $this->_storage->sdk_last_version ) &&
4959 version_compare( $this->_storage->sdk_last_version, '1.1.2', '<=' )
4960 ) {
4961 // Backward compatibility to 1.1.2
4962 $plugin_info['menu']['slug'] = isset( $plugin_info['menu_slug'] ) ?
4963 $plugin_info['menu_slug'] :
4964 $this->_slug;
4965 }
4966 }
4967
4968 $this->_menu = FS_Admin_Menu_Manager::instance(
4969 $this->_module_id,
4970 $this->_module_type,
4971 $this->get_unique_affix()
4972 );
4973
4974 $this->_menu->init( $plugin_info['menu'], $this->is_addon() );
4975
4976 $this->_has_addons = $this->get_bool_option( $plugin_info, 'has_addons', false );
4977 $this->_has_paid_plans = $this->get_bool_option( $plugin_info, 'has_paid_plans', true );
4978 $this->_has_premium_version = $this->get_bool_option( $plugin_info, 'has_premium_version', $this->_has_paid_plans );
4979 $this->_ignore_pending_mode = $this->get_bool_option( $plugin_info, 'ignore_pending_mode', false );
4980 $this->_is_org_compliant = $this->get_bool_option( $plugin_info, 'is_org_compliant', true );
4981 $this->_is_premium_only = $this->get_bool_option( $plugin_info, 'is_premium_only', false );
4982 if ( $this->_is_premium_only ) {
4983 // If premium only plugin, disable anonymous mode.
4984 $this->_enable_anonymous = false;
4985 $this->_anonymous_mode = false;
4986 } else {
4987 $this->_enable_anonymous = $this->get_bool_option( $plugin_info, 'enable_anonymous', true );
4988 $this->_anonymous_mode = $this->get_bool_option( $plugin_info, 'anonymous_mode', false );
4989 }
4990 $this->_permissions = $this->get_option( $plugin_info, 'permissions', array() );
4991
4992 if ( ! empty( $plugin_info['trial'] ) ) {
4993 $this->_trial_days = $this->get_numeric_option(
4994 $plugin_info['trial'],
4995 'days',
4996 // Default to 0 - trial without days specification.
4997 0
4998 );
4999
5000 $this->_is_trial_require_payment = $this->get_bool_option( $plugin_info['trial'], 'is_require_payment', false );
5001 }
5002 }
5003
5004 /**
5005 * @param string[] $options
5006 * @param string $key
5007 * @param mixed $default
5008 *
5009 * @return bool
5010 */
5011 private function get_option( &$options, $key, $default = false ) {
5012 return ! empty( $options[ $key ] ) ? $options[ $key ] : $default;
5013 }
5014
5015 private function get_bool_option( &$options, $key, $default = false ) {
5016 return isset( $options[ $key ] ) && is_bool( $options[ $key ] ) ? $options[ $key ] : $default;
5017 }
5018
5019 private function get_numeric_option( &$options, $key, $default = false ) {
5020 return isset( $options[ $key ] ) && is_numeric( $options[ $key ] ) ? $options[ $key ] : $default;
5021 }
5022
5023 /**
5024 * Gate keeper.
5025 *
5026 * @author Vova Feldman (@svovaf)
5027 * @since 1.1.7.3
5028 *
5029 * @return bool
5030 */
5031 private function should_stop_execution() {
5032 if ( empty( $this->_storage->was_plugin_loaded ) ) {
5033 /**
5034 * Don't execute Freemius until plugin was fully loaded at least once,
5035 * to give the opportunity for the activation hook to run before pinging
5036 * the API for connectivity test. This logic is relevant for the
5037 * identification of new plugin install vs. plugin update.
5038 *
5039 * @author Vova Feldman (@svovaf)
5040 * @since 1.1.9
5041 */
5042 return true;
5043 }
5044
5045 if ( $this->is_activation_mode() ) {
5046 if ( ! is_admin() ) {
5047 /**
5048 * If in activation mode, don't execute Freemius outside of the
5049 * admin dashboard.
5050 *
5051 * @author Vova Feldman (@svovaf)
5052 * @since 1.1.7.3
5053 */
5054 return true;
5055 }
5056
5057 if ( ! WP_FS__IS_HTTP_REQUEST ) {
5058 /**
5059 * If in activation and executed without HTTP context (e.g. CLI, Cronjob),
5060 * then don't start Freemius.
5061 *
5062 * @author Vova Feldman (@svovaf)
5063 * @since 1.1.6.3
5064 *
5065 * @link https://wordpress.org/support/topic/errors-in-the-freemius-class-when-running-in-wordpress-in-cli
5066 */
5067 return true;
5068 }
5069
5070 if ( self::is_cron() ) {
5071 /**
5072 * If in activation mode, don't execute Freemius during wp crons
5073 * (wp crons have HTTP context - called as HTTP request).
5074 *
5075 * @author Vova Feldman (@svovaf)
5076 * @since 1.1.7.3
5077 */
5078 return true;
5079 }
5080
5081 if ( self::is_ajax() &&
5082 ! $this->_admin_notices->has_sticky( 'failed_connect_api_first' ) &&
5083 ! $this->_admin_notices->has_sticky( 'failed_connect_api' )
5084 ) {
5085 /**
5086 * During activation, if running in AJAX mode, unless there's a sticky
5087 * connectivity issue notice, don't run Freemius.
5088 *
5089 * @author Vova Feldman (@svovaf)
5090 * @since 1.1.7.3
5091 */
5092 return true;
5093 }
5094 }
5095
5096 return false;
5097 }
5098
5099 /**
5100 * Triggered after code type has changed.
5101 *
5102 * @author Vova Feldman (@svovaf)
5103 * @since 1.1.9.1
5104 */
5105 function _after_code_type_change() {
5106 $this->_logger->entrance();
5107
5108 if ( $this->is_theme() ) {
5109 // Expire the cache of the previous tabs since the theme may
5110 // have setting updates after code type has changed.
5111 $this->_cache->expire( 'tabs' );
5112 $this->_cache->expire( 'tabs_stylesheets' );
5113 }
5114
5115 if ( $this->is_registered() ) {
5116 if ( ! $this->is_addon() ) {
5117 add_action(
5118 is_admin() ? 'admin_init' : 'init',
5119 array( &$this, '_plugin_code_type_changed' )
5120 );
5121 }
5122
5123 if ( $this->is_premium() ) {
5124 // Purge cached payments after switching to the premium version.
5125 // @todo This logic doesn't handle purging the cache for serviceware module upgrade.
5126 $this->get_api_user_scope()->purge_cache( "/plugins/{$this->_module_id}/payments.json?include_addons=true" );
5127 }
5128 }
5129 }
5130
5131 /**
5132 * Handles plugin's code type change (free <--> premium).
5133 *
5134 * @author Vova Feldman (@svovaf)
5135 * @since 1.0.9
5136 */
5137 function _plugin_code_type_changed() {
5138 $this->_logger->entrance();
5139
5140 if ( $this->is_premium() ) {
5141 $this->reconnect_locally();
5142
5143 // Activated premium code.
5144 $this->do_action( 'after_premium_version_activation' );
5145
5146 // Remove all sticky messages related to download of the premium version.
5147 $this->_admin_notices->remove_sticky( array(
5148 'trial_started',
5149 'plan_upgraded',
5150 'plan_changed',
5151 'license_activated',
5152 ) );
5153
5154 $notice = '';
5155 if ( ! $this->is_only_premium() ) {
5156 $notice = sprintf( $this->get_text_inline( 'Premium %s version was successfully activated.', 'premium-activated-message' ), $this->_module_type );
5157 }
5158
5159 $license_notice = $this->get_license_network_activation_notice();
5160 if ( ! empty( $license_notice ) ) {
5161 $notice .= ' ' . $license_notice;
5162 }
5163
5164 if ( ! empty( $notice ) ) {
5165 $this->_admin_notices->add_sticky(
5166 trim( $notice ),
5167 'premium_activated',
5168 $this->get_text_x_inline( 'W00t',
5169 'Used to express elation, enthusiasm, or triumph (especially in electronic communication).', 'woot' ) . '!'
5170 );
5171 }
5172 } else {
5173 // Remove sticky message related to premium code activation.
5174 $this->_admin_notices->remove_sticky( 'premium_activated' );
5175
5176 // Activated free code (after had the premium before).
5177 $this->do_action( 'after_free_version_reactivation' );
5178
5179 if ( $this->is_paying() && ! $this->is_premium() ) {
5180 $this->_admin_notices->add_sticky(
5181 sprintf(
5182 /* translators: %s: License type (e.g. you have a professional license) */
5183 $this->get_text_inline( 'You have a %s license.', 'you-have-x-license' ),
5184 $this->get_plan_title()
5185 ) . $this->get_complete_upgrade_instructions(),
5186 'plan_upgraded',
5187 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
5188 );
5189 }
5190 }
5191
5192 // Schedule code type changes event.
5193 $this->schedule_install_sync();
5194
5195 /**
5196 * Unregister the uninstall hook for the other version of the plugin (with different code type) to avoid
5197 * triggering a fatal error when uninstalling that plugin. For example, after deactivating the "free" version
5198 * of a specific plugin, its uninstall hook should be unregistered after the "premium" version has been
5199 * activated. If we don't do that, a fatal error will occur when we try to uninstall the "free" version since
5200 * the main file of the "free" version will be loaded first before calling the hooked callback. Since the
5201 * free and premium versions are almost identical (same class or have same functions), a fatal error like
5202 * "Cannot redeclare class MyClass" or "Cannot redeclare my_function()" will occur.
5203 */
5204 $this->unregister_uninstall_hook();
5205
5206 $this->clear_module_main_file_cache();
5207
5208 // Update is_premium of latest version.
5209 $this->_storage->prev_is_premium = $this->_plugin->is_premium;
5210 }
5211
5212 #endregion
5213
5214 #----------------------------------------------------------------------------------
5215 #region Add-ons
5216 #----------------------------------------------------------------------------------
5217
5218 /**
5219 * Check if add-on installed and activated on site.
5220 *
5221 * @author Vova Feldman (@svovaf)
5222 * @since 1.0.6
5223 *
5224 * @param string|number $id_or_slug
5225 * @param bool|null $is_premium Since 1.2.1.7 can check for specified add-on version.
5226 *
5227 * @return bool
5228 */
5229 function is_addon_activated( $id_or_slug, $is_premium = null ) {
5230 $this->_logger->entrance();
5231
5232 $addon_id = self::get_module_id( $id_or_slug );
5233 $is_activated = self::has_instance( $addon_id );
5234
5235 if ( ! $is_activated ) {
5236 return false;
5237 }
5238
5239 if ( is_bool( $is_premium ) ) {
5240 // Check if the specified code version is activate.
5241 $addon = $this->get_addon_instance( $addon_id );
5242 $is_activated = ( $is_premium === $addon->is_premium() );
5243 }
5244
5245 return $is_activated;
5246 }
5247
5248 /**
5249 * Check if add-on was connected to install
5250 *
5251 * @author Vova Feldman (@svovaf)
5252 * @since 1.1.7
5253 *
5254 * @param string|number $id_or_slug
5255 *
5256 * @return bool
5257 */
5258 function is_addon_connected( $id_or_slug ) {
5259 $this->_logger->entrance();
5260
5261 $sites = self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN );
5262
5263 $addon_id = self::get_module_id( $id_or_slug );
5264 $addon = $this->get_addon( $addon_id );
5265 $slug = $addon->slug;
5266 if ( ! isset( $sites[ $slug ] ) ) {
5267 return false;
5268 }
5269
5270 $site = $sites[ $slug ];
5271
5272 $plugin = FS_Plugin_Manager::instance( $addon_id )->get();
5273
5274 if ( $plugin->parent_plugin_id != $this->_plugin->id ) {
5275 // The given slug do NOT belong to any of the plugin's add-ons.
5276 return false;
5277 }
5278
5279 return ( is_object( $site ) &&
5280 is_numeric( $site->id ) &&
5281 is_numeric( $site->user_id ) &&
5282 FS_Plugin_Plan::is_valid_id( $site->plan_id )
5283 );
5284 }
5285
5286 /**
5287 * Determines if add-on installed.
5288 *
5289 * NOTE: This is a heuristic and only works if the folder/file named as the slug.
5290 *
5291 * @author Vova Feldman (@svovaf)
5292 * @since 1.0.6
5293 *
5294 * @param string|number $id_or_slug
5295 *
5296 * @return bool
5297 */
5298 function is_addon_installed( $id_or_slug ) {
5299 $this->_logger->entrance();
5300
5301 $addon_id = self::get_module_id( $id_or_slug );
5302
5303 return file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $this->get_addon_basename( $addon_id ) ) );
5304 }
5305
5306 /**
5307 * Get add-on basename.
5308 *
5309 * @author Vova Feldman (@svovaf)
5310 * @since 1.0.6
5311 *
5312 * @param string|number $id_or_slug
5313 *
5314 * @return string
5315 */
5316 function get_addon_basename( $id_or_slug ) {
5317 $addon_id = self::get_module_id( $id_or_slug );
5318
5319 if ( $this->is_addon_activated( $addon_id ) ) {
5320 return self::instance( $addon_id )->get_plugin_basename();
5321 }
5322
5323 $addon = $this->get_addon( $addon_id );
5324 $premium_basename = "{$addon->premium_slug}/{$addon->slug}.php";
5325
5326 if ( file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $premium_basename ) ) ) {
5327 return $premium_basename;
5328 }
5329
5330 $all_plugins = $this->get_all_plugins();
5331
5332 foreach ( $all_plugins as $basename => $data ) {
5333 if ( $addon->slug === $data['slug'] ||
5334 $addon->premium_slug === $data['slug']
5335 ) {
5336 return $basename;
5337 }
5338 }
5339
5340 $free_basename = "{$addon->slug}/{$addon->slug}.php";
5341
5342 return $free_basename;
5343 }
5344
5345 /**
5346 * Get installed add-ons instances.
5347 *
5348 * @author Vova Feldman (@svovaf)
5349 * @since 1.0.6
5350 *
5351 * @return Freemius[]
5352 */
5353 function get_installed_addons() {
5354 $installed_addons = array();
5355 foreach ( self::$_instances as $instance ) {
5356 if ( $instance->is_addon() && is_object( $instance->_parent_plugin ) ) {
5357 if ( $this->_plugin->id == $instance->_parent_plugin->id ) {
5358 $installed_addons[] = $instance;
5359 }
5360 }
5361 }
5362
5363 return $installed_addons;
5364 }
5365
5366 /**
5367 * Check if any add-ons of the plugin are installed.
5368 *
5369 * @author Leo Fajardo (@leorw)
5370 * @since 1.1.1
5371 *
5372 * @return bool
5373 */
5374 function has_installed_addons() {
5375 if ( ! $this->has_addons() ) {
5376 return false;
5377 }
5378
5379 foreach ( self::$_instances as $instance ) {
5380 if ( $instance->is_addon() && is_object( $instance->_parent_plugin ) ) {
5381 if ( $this->_plugin->id == $instance->_parent_plugin->id ) {
5382 return true;
5383 }
5384 }
5385 }
5386
5387 return false;
5388 }
5389
5390 /**
5391 * Tell Freemius that the current plugin is an add-on.
5392 *
5393 * @author Vova Feldman (@svovaf)
5394 * @since 1.0.6
5395 *
5396 * @param number $parent_plugin_id The parent plugin ID
5397 */
5398 function init_addon( $parent_plugin_id ) {
5399 $this->_plugin->parent_plugin_id = $parent_plugin_id;
5400 }
5401
5402 /**
5403 * @author Vova Feldman (@svovaf)
5404 * @since 1.0.6
5405 *
5406 * @return bool
5407 */
5408 function is_addon() {
5409 return isset( $this->_plugin->parent_plugin_id ) && is_numeric( $this->_plugin->parent_plugin_id );
5410 }
5411
5412 /**
5413 * Deactivate add-on if it's premium only and the user does't have a valid license.
5414 *
5415 * @param bool $is_after_trial_cancel
5416 *
5417 * @return bool If add-on was deactivated.
5418 */
5419 private function deactivate_premium_only_addon_without_license( $is_after_trial_cancel = false ) {
5420 if ( ! $this->has_free_plan() &&
5421 ! $this->has_features_enabled_license() &&
5422 ! $this->_has_premium_license()
5423 ) {
5424 if ( $this->is_registered() ) {
5425 // IF wrapper is turned off because activation_timestamp is currently only stored for plugins (not addons).
5426 // if (empty($this->_storage->activation_timestamp) ||
5427 // (WP_FS__SCRIPT_START_TIME - $this->_storage->activation_timestamp) > 30
5428 // ) {
5429 /**
5430 * @todo When it's first fail, there's no reason to try and re-sync because the licenses were just synced after initial activation.
5431 *
5432 * Retry syncing the user add-on licenses.
5433 */
5434 // Sync licenses.
5435 $this->_sync_licenses();
5436 // }
5437
5438 // Try to activate premium license.
5439 $this->_activate_license( true );
5440 }
5441
5442 if ( ! $this->has_free_plan() &&
5443 ! $this->has_features_enabled_license() &&
5444 ! $this->_has_premium_license()
5445 ) {
5446 // @todo Check if deactivate plugins also call the deactivation hook.
5447
5448 $this->_parent->_admin_notices->add_sticky(
5449 sprintf(
5450 ( $is_after_trial_cancel ?
5451 $this->_parent->get_text_inline(
5452 '%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.',
5453 'addon-trial-cancelled-message'
5454 ) :
5455 $this->_parent->get_text_inline(
5456 '%s is a premium only add-on. You have to purchase a license first before activating the plugin.',
5457 'addon-no-license-message'
5458 )
5459 ),
5460 '<b>' . $this->_plugin->title . '</b>'
5461 ) . ' ' . sprintf(
5462 '<a href="%s" aria-label="%s" class="button button-primary" style="margin-left: 10px; vertical-align: middle;">%s &nbsp;&#10140;</a>',
5463 $this->_parent->addon_url( $this->_slug ),
5464 esc_attr( sprintf( $this->_parent->get_text_inline( 'More information about %s', 'more-information-about-x' ), $this->_plugin->title ) ),
5465 $this->_parent->get_text_inline( 'Purchase License', 'purchase-license' )
5466 ),
5467 'no_addon_license_' . $this->_slug,
5468 ( $is_after_trial_cancel ? '' : $this->_parent->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...' ),
5469 ( $is_after_trial_cancel ? 'success' : 'error' )
5470 );
5471
5472 deactivate_plugins( array( $this->_plugin_basename ), true );
5473
5474 return true;
5475 }
5476 }
5477
5478 return false;
5479 }
5480
5481 #endregion
5482
5483 #----------------------------------------------------------------------------------
5484 #region Sandbox
5485 #----------------------------------------------------------------------------------
5486
5487 /**
5488 * Set Freemius into sandbox mode for debugging.
5489 *
5490 * @author Vova Feldman (@svovaf)
5491 * @since 1.0.4
5492 *
5493 * @param string $secret_key
5494 */
5495 function init_sandbox( $secret_key ) {
5496 $this->_plugin->secret_key = $secret_key;
5497
5498 // Update plugin details.
5499 FS_Plugin_Manager::instance( $this->_module_id )->update( $this->_plugin, true );
5500 }
5501
5502 /**
5503 * Check if running payments in sandbox mode.
5504 *
5505 * @author Vova Feldman (@svovaf)
5506 * @since 1.0.4
5507 *
5508 * @return bool
5509 */
5510 function is_payments_sandbox() {
5511 return ( ! $this->is_live() ) || isset( $this->_plugin->secret_key );
5512 }
5513
5514 #endregion
5515
5516 /**
5517 * Check if running test vs. live plugin.
5518 *
5519 * @author Vova Feldman (@svovaf)
5520 * @since 1.0.5
5521 *
5522 * @return bool
5523 */
5524 function is_live() {
5525 return $this->_plugin->is_live;
5526 }
5527
5528 /**
5529 * Check if super-admin skipped connection for all sites in the network.
5530 *
5531 * @author Vova Feldman (@svovaf)
5532 * @since 2.0.0
5533 */
5534 function is_network_anonymous() {
5535 if ( ! $this->_is_network_active ) {
5536 return false;
5537 }
5538
5539 $is_anonymous_ms = $this->_storage->get( 'is_anonymous_ms' );
5540
5541 if ( empty( $is_anonymous_ms ) ) {
5542 return false;
5543 }
5544
5545 return $is_anonymous_ms['is'];
5546 }
5547
5548 /**
5549 * Check if super-admin opted-in for all sites in the network.
5550 *
5551 * @author Vova Feldman (@svovaf)
5552 * @since 2.0.0
5553 */
5554 function is_network_connected() {
5555 if ( ! $this->_is_network_active ) {
5556 return false;
5557 }
5558
5559 return $this->_storage->get( 'is_network_connected' );
5560 }
5561
5562 /**
5563 * Check if the user skipped connecting the account with Freemius.
5564 *
5565 * @author Vova Feldman (@svovaf)
5566 * @since 1.0.7
5567 *
5568 * @return bool
5569 */
5570 function is_anonymous() {
5571 if ( ! isset( $this->_is_anonymous ) ) {
5572 if ( $this->is_network_anonymous() ) {
5573 $this->_is_anonymous = true;
5574 } else if ( ! fs_is_network_admin() ) {
5575 if ( ! isset( $this->_storage->is_anonymous ) ) {
5576 // Not skipped.
5577 $this->_is_anonymous = false;
5578 } else if ( is_bool( $this->_storage->is_anonymous ) ) {
5579 // For back compatibility, since the variable was boolean before.
5580 $this->_is_anonymous = $this->_storage->is_anonymous;
5581
5582 // Upgrade stored data format to 1.1.3 format.
5583 $this->set_anonymous_mode( $this->_storage->is_anonymous );
5584 } else {
5585 // Version 1.1.3 and later.
5586 $this->_is_anonymous = $this->_storage->is_anonymous['is'];
5587 }
5588 }
5589 }
5590
5591 return $this->_is_anonymous;
5592 }
5593
5594 /**
5595 * Check if the user skipped the connection of a specified site.
5596 *
5597 * @author Vova Feldman (@svovaf)
5598 * @since 2.0.0
5599 *
5600 * @param int $blog_id
5601 *
5602 * @return bool
5603 */
5604 function is_anonymous_site( $blog_id = 0 ) {
5605 if ( $this->is_network_anonymous() ) {
5606 return true;
5607 }
5608
5609 $is_anonymous = $this->_storage->get( 'is_anonymous', false, $blog_id );
5610
5611 if ( empty( $is_anonymous ) ) {
5612 return false;
5613 }
5614
5615 return $is_anonymous['is'];
5616 }
5617
5618 /**
5619 * Check if user connected his account and install pending email activation.
5620 *
5621 * @author Vova Feldman (@svovaf)
5622 * @since 1.0.7
5623 *
5624 * @return bool
5625 */
5626 function is_pending_activation() {
5627 return $this->_storage->get( 'is_pending_activation', false );
5628 }
5629
5630 /**
5631 * Check if plugin must be WordPress.org compliant.
5632 *
5633 * @since 1.0.7
5634 *
5635 * @return bool
5636 */
5637 function is_org_repo_compliant() {
5638 return $this->_is_org_compliant;
5639 }
5640
5641 #--------------------------------------------------------------------------------
5642 #region WP Cron Common
5643 #--------------------------------------------------------------------------------
5644
5645 /**
5646 * @author Vova Feldman (@svovaf)
5647 * @since 2.0.0
5648 *
5649 * @param string $name Cron name.
5650 *
5651 * @return object
5652 */
5653 private function get_cron_data( $name ) {
5654 $this->_logger->entrance( $name );
5655
5656 /**
5657 * @var object $cron_data
5658 */
5659 return $this->_storage->get( "{$name}_cron", null );
5660 }
5661
5662 /**
5663 * @author Vova Feldman (@svovaf)
5664 * @since 2.0.0
5665 *
5666 * @param string $name Cron name.
5667 */
5668 private function clear_cron_data( $name ) {
5669 $this->_logger->entrance( $name );
5670
5671 $this->_storage->remove( "{$name}_cron" );
5672 }
5673
5674 /**
5675 * @author Vova Feldman (@svovaf)
5676 * @since 2.0.0
5677 *
5678 * @param string $name Cron name.
5679 * @param int $cron_blog_id The cron executing blog ID.
5680 */
5681 private function set_cron_data( $name, $cron_blog_id = 0 ) {
5682 $this->_logger->entrance( $name );
5683
5684 $this->_storage->store( "{$name}_cron", (object) array(
5685 'version' => $this->get_plugin_version(),
5686 'blog_id' => $cron_blog_id,
5687 'sdk_version' => $this->version,
5688 'timestamp' => WP_FS__SCRIPT_START_TIME,
5689 'on' => true,
5690 ) );
5691 }
5692
5693 /**
5694 * Get the cron's executing blog ID.
5695 *
5696 * @author Vova Feldman (@svovaf)
5697 * @since 2.0.0
5698 *
5699 * @param string $name Cron name.
5700 *
5701 * @return int
5702 */
5703 private function get_cron_blog_id( $name ) {
5704 $this->_logger->entrance( $name );
5705
5706 /**
5707 * @var object $cron_data
5708 */
5709 $cron_data = $this->get_cron_data( $name );
5710
5711 return ( is_object( $cron_data ) && is_numeric( $cron_data->blog_id ) ) ?
5712 $cron_data->blog_id :
5713 0;
5714 }
5715
5716 /**
5717 * @author Vova Feldman (@svovaf)
5718 * @since 2.0.0
5719 *
5720 * @param string $name Cron name.
5721 *
5722 * @return bool
5723 */
5724 private function is_cron_on( $name ) {
5725 $this->_logger->entrance( $name );
5726
5727 /**
5728 * @var object $cron_data
5729 */
5730 $cron_data = $this->get_cron_data( $name );
5731
5732 return ( ! is_null( $cron_data ) && true === $cron_data->on );
5733 }
5734
5735 /**
5736 * Unix timestamp for previous cron execution or false if never executed.
5737 *
5738 * @author Vova Feldman (@svovaf)
5739 * @since 2.0.0
5740 *
5741 * @param string $name Cron name.
5742 *
5743 * @return int|false
5744 */
5745 private function cron_last_execution( $name ) {
5746 $this->_logger->entrance( $name );
5747
5748 return $this->_storage->get( "{$name}_timestamp" );
5749 }
5750
5751 /**
5752 * Set cron execution time to now.
5753 *
5754 * @author Vova Feldman (@svovaf)
5755 * @since 2.0.0
5756 *
5757 * @param string $name Cron name.
5758 */
5759 private function set_cron_execution_timestamp( $name ) {
5760 $this->_logger->entrance( $name );
5761
5762 $this->_storage->store( "{$name}_timestamp", time() );
5763 }
5764
5765 /**
5766 * Sets the keepalive time to now.
5767 *
5768 * @author Leo Fajardo (@leorw)
5769 * @since 2.2.3
5770 *
5771 * @param bool|null $use_network_level_storage
5772 */
5773 private function set_keepalive_timestamp( $use_network_level_storage = null ) {
5774 $this->_logger->entrance();
5775
5776 $this->_storage->store( 'keepalive_timestamp', time(), $use_network_level_storage );
5777 }
5778
5779 /**
5780 * Check if cron was executed in the last $period of seconds.
5781 *
5782 * @author Vova Feldman (@svovaf)
5783 * @since 2.0.0
5784 *
5785 * @param string $name Cron name.
5786 * @param int $period In seconds
5787 *
5788 * @return bool
5789 */
5790 private function is_cron_executed( $name, $period = WP_FS__TIME_24_HOURS_IN_SEC ) {
5791 $this->_logger->entrance( $name );
5792
5793 $last_execution = $this->cron_last_execution( $name );
5794
5795 if ( ! is_numeric( $last_execution ) ) {
5796 return false;
5797 }
5798
5799 return ( $last_execution > ( WP_FS__SCRIPT_START_TIME - $period ) );
5800 }
5801
5802 /**
5803 * WP Cron is executed on a site level. When running in a multisite network environment
5804 * with the network integration activated, for optimization reasons, we are consolidating
5805 * the installs data sync cron to be executed only from a single site.
5806 *
5807 * @author Vova Feldman (@svovaf)
5808 * @since 2.0.0
5809 *
5810 * @param int $except_blog_id Target any except the excluded blog ID.
5811 *
5812 * @return int
5813 */
5814 private function get_cron_target_blog_id( $except_blog_id = 0 ) {
5815 if ( ! is_multisite() ) {
5816 return 0;
5817 }
5818
5819 if ( $this->_is_network_active &&
5820 is_numeric( $this->_storage->network_install_blog_id ) &&
5821 $except_blog_id != $this->_storage->network_install_blog_id &&
5822 self::is_site_active( $this->_storage->network_install_blog_id )
5823 ) {
5824 // Try to run cron from the main network blog.
5825 $install = $this->get_install_by_blog_id( $this->_storage->network_install_blog_id );
5826
5827 if ( is_object( $install ) &&
5828 ( $this->is_premium() || $install->is_tracking_allowed() )
5829 ) {
5830 return $this->_storage->network_install_blog_id;
5831 }
5832 }
5833
5834 // Get first opted-in blog ID with active tracking.
5835 $installs = $this->get_blog_install_map();
5836 foreach ( $installs as $blog_id => $install ) {
5837 if ( $except_blog_id != $blog_id &&
5838 self::is_site_active( $blog_id ) &&
5839 ( $this->is_premium() || $install->is_tracking_allowed() )
5840 ) {
5841 return $blog_id;
5842 }
5843 }
5844
5845 return 0;
5846 }
5847
5848 /**
5849 * @author Vova Feldman (@svovaf)
5850 * @since 2.0.0
5851 *
5852 * @param string $name Cron name.
5853 * @param string $action_tag Callback action tag.
5854 * @param bool $is_network_clear If set to TRUE, clear sync cron even if there are installs that are still connected.
5855 */
5856 private function clear_cron( $name, $action_tag = '', $is_network_clear = false ) {
5857 $this->_logger->entrance( $name );
5858
5859 if ( ! $this->is_cron_on( $name ) ) {
5860 return;
5861 }
5862
5863 $clear_cron = true;
5864 if ( ! $is_network_clear && $this->_is_network_active ) {
5865 $installs = $this->get_blog_install_map();
5866
5867 foreach ( $installs as $blog_id => $install ) {
5868 /**
5869 * @var FS_Site $install
5870 */
5871 if ( $install->is_tracking_allowed() ) {
5872 $clear_cron = false;
5873 break;
5874 }
5875 }
5876 }
5877
5878 if ( ! $clear_cron ) {
5879 return;
5880 }
5881
5882 /**
5883 * @var object $cron_data
5884 */
5885 $cron_data = $this->get_cron_data( $name );
5886
5887 $cron_blog_id = is_object( $cron_data ) && isset( $cron_data->blog_id ) ?
5888 $cron_data->blog_id :
5889 0;
5890
5891 $this->clear_cron_data( $name );
5892
5893 if ( 0 < $cron_blog_id ) {
5894 switch_to_blog( $cron_blog_id );
5895 }
5896
5897 if ( empty( $action_tag ) ) {
5898 $action_tag = $name;
5899 }
5900
5901 wp_clear_scheduled_hook( $this->get_action_tag( $action_tag ) );
5902
5903 if ( 0 < $cron_blog_id ) {
5904 restore_current_blog();
5905 }
5906 }
5907
5908 /**
5909 * Unix timestamp for next cron execution or false if not scheduled.
5910 *
5911 * @author Vova Feldman (@svovaf)
5912 * @since 2.0.0
5913 *
5914 * @param string $name Cron name.
5915 * @param string $action_tag Callback action tag.
5916 *
5917 * @return int|false
5918 */
5919 private function get_next_scheduled_cron( $name, $action_tag = '' ) {
5920 $this->_logger->entrance( $name );
5921
5922 if ( ! $this->is_cron_on( $name ) ) {
5923 return false;
5924 }
5925
5926 /**
5927 * @var object $cron_data
5928 */
5929 $cron_data = $this->get_cron_data( $name );
5930
5931 $cron_blog_id = is_object( $cron_data ) && isset( $cron_data->blog_id ) ?
5932 $cron_data->blog_id :
5933 0;
5934
5935 if ( 0 < $cron_blog_id ) {
5936 switch_to_blog( $cron_blog_id );
5937 }
5938
5939 if ( empty( $action_tag ) ) {
5940 $action_tag = $name;
5941 }
5942
5943 $next_scheduled = wp_next_scheduled( $this->get_action_tag( $action_tag ) );
5944
5945 if ( 0 < $cron_blog_id ) {
5946 restore_current_blog();
5947 }
5948
5949 return $next_scheduled;
5950 }
5951
5952 /**
5953 * @author Vova Feldman (@svovaf)
5954 * @since 2.0.0
5955 *
5956 * @param string $name Cron name.
5957 * @param string $action_tag Callback action tag.
5958 * @param string $recurrence 'single' or 'daily'.
5959 * @param int $start_at Defaults to now.
5960 * @param bool $randomize_start If true, schedule first job randomly during the next 12 hours. Otherwise, schedule job to start right away.
5961 * @param int $except_blog_id Target any except the excluded blog ID.
5962 */
5963 private function schedule_cron(
5964 $name,
5965 $action_tag = '',
5966 $recurrence = 'single',
5967 $start_at = WP_FS__SCRIPT_START_TIME,
5968 $randomize_start = true,
5969 $except_blog_id = 0
5970 ) {
5971 $this->_logger->entrance( $name );
5972
5973 $this->clear_cron( $name, $action_tag, true );
5974
5975 $cron_blog_id = $this->get_cron_target_blog_id( $except_blog_id );
5976
5977 if ( is_multisite() && 0 == $cron_blog_id ) {
5978 // Don't schedule cron since couldn't find a target blog.
5979 return;
5980 }
5981
5982 if ( 0 < $cron_blog_id ) {
5983 switch_to_blog( $cron_blog_id );
5984 }
5985
5986 if ( 'daily' === $recurrence ) {
5987 if ( $randomize_start ) {
5988 // Schedule first sync with a random 12 hour time range from now.
5989 $start_at += rand( 0, ( WP_FS__TIME_24_HOURS_IN_SEC / 2 ) );
5990 }
5991
5992 // Schedule daily WP cron.
5993 wp_schedule_event(
5994 $start_at,
5995 'daily',
5996 $this->get_action_tag( $action_tag )
5997 );
5998 } else if ( 'single' === $recurrence ) {
5999 // Schedule single cron.
6000 wp_schedule_single_event(
6001 $start_at,
6002 $this->get_action_tag( $action_tag )
6003 );
6004 }
6005
6006 $this->set_cron_data( $name, $cron_blog_id );
6007
6008 if ( 0 < $cron_blog_id ) {
6009 restore_current_blog();
6010 }
6011 }
6012
6013 /**
6014 * Consolidated cron execution for performance optimization. The max number of API requests is based on the number of unique opted-in users.
6015 * that doesn't halt page loading.
6016 *
6017 * @author Vova Feldman (@svovaf)
6018 * @since 2.0.0
6019 *
6020 * @param string $name Cron name.
6021 * @param callable $callable The function that should be executed.
6022 */
6023 private function execute_cron( $name, $callable ) {
6024 $this->_logger->entrance( $name );
6025
6026 // Store the last time data sync was executed.
6027 $this->set_cron_execution_timestamp( $name );
6028
6029 // Check if API is temporary down.
6030 if ( FS_Api::is_temporary_down() ) {
6031 return;
6032 }
6033
6034 // @todo Add logic that identifies API latency, and reschedule the next background sync randomly between 8-16 hours.
6035
6036 $users_2_blog_ids = array();
6037
6038 if ( ! is_multisite() ) {
6039 // Add dummy blog.
6040 $users_2_blog_ids[0] = array( 0 );
6041 } else {
6042 $installs = $this->get_blog_install_map();
6043 foreach ( $installs as $blog_id => $install ) {
6044 if ( $this->is_premium() || $install->is_tracking_allowed() ) {
6045 if ( ! isset( $users_2_blog_ids[ $install->user_id ] ) ) {
6046 $users_2_blog_ids[ $install->user_id ] = array();
6047 }
6048
6049 $users_2_blog_ids[ $install->user_id ][] = $blog_id;
6050 }
6051 }
6052 }
6053
6054 $current_blog_id = get_current_blog_id();
6055
6056 foreach ( $users_2_blog_ids as $user_id => $blog_ids ) {
6057 if ( 0 < $blog_ids[0] ) {
6058 $this->switch_to_blog( $blog_ids[0] );
6059 }
6060
6061 call_user_func_array( $callable, array( $blog_ids, ( is_multisite() ? $current_blog_id : null ) ) );
6062
6063 foreach ( $blog_ids as $blog_id ) {
6064 $this->do_action( "after_{$name}_cron", $blog_id );
6065 }
6066 }
6067
6068 if ( is_multisite() ) {
6069 $this->switch_to_blog( $current_blog_id );
6070
6071 $this->do_action( "after_{$name}_cron_multisite" );
6072 }
6073 }
6074
6075 #endregion
6076
6077 #----------------------------------------------------------------------------------
6078 #region Daily Sync Cron
6079 #----------------------------------------------------------------------------------
6080
6081
6082 /**
6083 * @author Vova Feldman (@svovaf)
6084 * @since 2.0.0
6085 *
6086 * @return bool
6087 */
6088 private function is_sync_cron_scheduled() {
6089 return $this->is_cron_on( 'sync' );
6090 }
6091
6092 /**
6093 * Get the sync cron's executing blog ID.
6094 *
6095 * @author Vova Feldman (@svovaf)
6096 * @since 2.0.0
6097 *
6098 * @return int
6099 */
6100 private function get_sync_cron_blog_id() {
6101 return $this->get_cron_blog_id( 'sync' );
6102 }
6103
6104 /**
6105 * @author Vova Feldman (@svovaf)
6106 * @since 1.1.7.3
6107 */
6108 private function run_manual_sync() {
6109 self::require_pluggable_essentials();
6110
6111 if ( ! $this->is_user_admin() ) {
6112 return;
6113 }
6114
6115 // Run manual sync.
6116 $this->_sync_cron();
6117
6118 // Reschedule next cron to run 24 hours from now (performance optimization).
6119 $this->schedule_sync_cron( time() + WP_FS__TIME_24_HOURS_IN_SEC, false );
6120 }
6121
6122 /**
6123 * Data sync cron job. Replaces the background sync non blocking HTTP request
6124 * that doesn't halt page loading.
6125 *
6126 * @author Vova Feldman (@svovaf)
6127 * @since 1.1.7.3
6128 * @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.
6129 */
6130 function _sync_cron() {
6131 $this->_logger->entrance();
6132
6133 $this->execute_cron( 'sync', array( &$this, '_sync_cron_method' ) );
6134 }
6135
6136 /**
6137 * The actual data sync cron logic.
6138 *
6139 * @author Vova Feldman (@svovaf)
6140 * @since 2.0.0
6141 *
6142 * @param int[] $blog_ids
6143 * @param int|null $current_blog_id @since 2.2.3. This is passed from the `execute_cron` method and used by the
6144 * `_sync_plugin_license` method in order to switch to the previous blog when sending
6145 * updates for a single site in case `execute_cron` has switched to a different blog.
6146 */
6147 function _sync_cron_method( array $blog_ids, $current_blog_id = null ) {
6148 if ( $this->is_registered() ) {
6149 if ( $this->has_paid_plan() ) {
6150 // Initiate background plan sync.
6151 $this->_sync_license( true, false, $current_blog_id );
6152
6153 if ( $this->is_paying() ) {
6154 // Check for premium plugin updates.
6155 $this->check_updates( true );
6156 }
6157 } else {
6158 // Sync install(s) (only if something changed locally).
6159 if ( 1 < count( $blog_ids ) ) {
6160 $this->sync_installs();
6161 } else {
6162 $this->sync_install();
6163 }
6164 }
6165 }
6166 }
6167
6168 /**
6169 * Check if sync was executed in the last $period of seconds.
6170 *
6171 * @author Vova Feldman (@svovaf)
6172 * @since 1.1.7.3
6173 *
6174 * @param int $period In seconds
6175 *
6176 * @return bool
6177 */
6178 private function is_sync_executed( $period = WP_FS__TIME_24_HOURS_IN_SEC ) {
6179 return $this->is_cron_executed( 'sync', $period );
6180 }
6181
6182 /**
6183 * @author Vova Feldman (@svovaf)
6184 * @since 1.1.7.3
6185 *
6186 * @return bool
6187 */
6188 private function is_sync_cron_on() {
6189 return $this->is_cron_on( 'sync' );
6190 }
6191
6192 /**
6193 * @author Vova Feldman (@svovaf)
6194 * @since 1.1.7.3
6195 *
6196 * @param int $start_at Defaults to now.
6197 * @param bool $randomize_start If true, schedule first job randomly during the next 12 hours. Otherwise, schedule job to start right away.
6198 * @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.
6199 */
6200 private function schedule_sync_cron(
6201 $start_at = WP_FS__SCRIPT_START_TIME,
6202 $randomize_start = true,
6203 $except_blog_id = 0
6204 ) {
6205 $this->schedule_cron(
6206 'sync',
6207 'data_sync',
6208 'daily',
6209 $start_at,
6210 $randomize_start,
6211 $except_blog_id
6212 );
6213 }
6214
6215 /**
6216 * Add the actual sync function to the cron job hook.
6217 *
6218 * @author Vova Feldman (@svovaf)
6219 * @since 1.1.7.3
6220 */
6221 private function hook_callback_to_sync_cron() {
6222 $this->add_action( 'data_sync', array( &$this, '_sync_cron' ) );
6223 }
6224
6225 /**
6226 * @author Vova Feldman (@svovaf)
6227 * @since 1.1.7.3
6228 *
6229 * @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.
6230 */
6231 private function clear_sync_cron( $is_network_clear = false ) {
6232 $this->_logger->entrance();
6233
6234 $this->clear_cron( 'sync', 'data_sync', $is_network_clear );
6235 }
6236
6237 /**
6238 * Unix timestamp for next sync cron execution or false if not scheduled.
6239 *
6240 * @author Vova Feldman (@svovaf)
6241 * @since 1.1.7.3
6242 *
6243 * @return int|false
6244 */
6245 function next_sync_cron() {
6246 return $this->get_next_scheduled_cron( 'sync', 'data_sync' );
6247 }
6248
6249 /**
6250 * Unix timestamp for previous sync cron execution or false if never executed.
6251 *
6252 * @author Vova Feldman (@svovaf)
6253 * @since 1.1.7.3
6254 *
6255 * @return int|false
6256 */
6257 function last_sync_cron() {
6258 return $this->cron_last_execution( 'sync' );
6259 }
6260
6261 #endregion Daily Sync Cron ------------------------------------------------------------------
6262
6263 #----------------------------------------------------------------------------------
6264 #region Async Install Sync
6265 #----------------------------------------------------------------------------------
6266
6267 /**
6268 * @author Vova Feldman (@svovaf)
6269 * @since 1.1.7.3
6270 *
6271 * @return bool
6272 */
6273 private function is_install_sync_scheduled() {
6274 return $this->is_cron_on( 'install_sync' );
6275 }
6276
6277 /**
6278 * Get the sync cron's executing blog ID.
6279 *
6280 * @author Vova Feldman (@svovaf)
6281 * @since 2.0.0
6282 *
6283 * @return int
6284 */
6285 private function get_install_sync_cron_blog_id() {
6286 return $this->get_cron_blog_id( 'install_sync' );
6287 }
6288
6289 /**
6290 * Instead of running blocking install sync event, execute non blocking scheduled wp-cron.
6291 *
6292 * @author Vova Feldman (@svovaf)
6293 * @since 1.1.7.3
6294 *
6295 * @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.
6296 */
6297 private function schedule_install_sync( $except_blog_id = 0 ) {
6298 $this->schedule_cron( 'install_sync', 'install_sync', 'single', WP_FS__SCRIPT_START_TIME, false, $except_blog_id );
6299 }
6300
6301 /**
6302 * Unix timestamp for previous install sync cron execution or false if never executed.
6303 *
6304 * @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.
6305 *
6306 * @author Vova Feldman (@svovaf)
6307 * @since 1.1.7.3
6308 *
6309 * @return int|false
6310 */
6311 function last_install_sync() {
6312 return $this->cron_last_execution( 'install_sync' );
6313 }
6314
6315 /**
6316 * Unix timestamp for next install sync cron execution or false if not scheduled.
6317 *
6318 * @author Vova Feldman (@svovaf)
6319 * @since 1.1.7.3
6320 *
6321 * @return int|false
6322 */
6323 function next_install_sync() {
6324 return $this->get_next_scheduled_cron( 'install_sync', 'install_sync' );
6325 }
6326
6327 /**
6328 * Add the actual install sync function to the cron job hook.
6329 *
6330 * @author Vova Feldman (@svovaf)
6331 * @since 1.1.7.3
6332 */
6333 private function hook_callback_to_install_sync() {
6334 $this->add_action( 'install_sync', array( &$this, '_run_sync_install' ) );
6335 }
6336
6337 /**
6338 * @author Vova Feldman (@svovaf)
6339 * @since 1.1.7.3
6340 *
6341 * @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.
6342 */
6343 private function clear_install_sync_cron( $is_network_clear = false ) {
6344 $this->_logger->entrance();
6345
6346 $this->clear_cron( 'install_sync', 'install_sync', $is_network_clear );
6347 }
6348
6349 /**
6350 * @author Vova Feldman (@svovaf)
6351 * @since 1.1.7.3
6352 * @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.
6353 */
6354 public function _run_sync_install() {
6355 $this->_logger->entrance();
6356
6357 $this->execute_cron( 'sync', array( &$this, '_sync_install_cron_method' ) );
6358 }
6359
6360 /**
6361 * The actual install(s) sync cron logic.
6362 *
6363 * @author Vova Feldman (@svovaf)
6364 * @since 2.0.0
6365 *
6366 * @param int[] $blog_ids
6367 * @param int|null $current_blog_id
6368 */
6369 function _sync_install_cron_method( array $blog_ids, $current_blog_id = null ) {
6370 if ( $this->is_registered() ) {
6371 if ( 1 < count( $blog_ids ) ) {
6372 $this->sync_installs( array(), true );
6373 } else {
6374 $this->sync_install( array(), true );
6375 }
6376 }
6377 }
6378
6379 #endregion Async Install Sync ------------------------------------------------------------------
6380
6381 /**
6382 * Show a notice that activation is currently pending.
6383 *
6384 * @author Vova Feldman (@svovaf)
6385 * @since 1.0.7
6386 *
6387 * @param bool|string $email
6388 * @param bool $is_pending_trial Since 1.2.1.5
6389 */
6390 function _add_pending_activation_notice( $email = false, $is_pending_trial = false ) {
6391 if ( ! is_string( $email ) ) {
6392 $current_user = self::_get_current_wp_user();
6393 $email = $current_user->user_email;
6394 }
6395
6396 $this->_admin_notices->add_sticky(
6397 sprintf(
6398 $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' ),
6399 '<b>' . $this->get_plugin_name() . '</b>',
6400 '<b>' . $email . '</b>',
6401 ( $is_pending_trial ?
6402 $this->get_text_inline( 'start the trial', 'start-the-trial' ) :
6403 $this->get_text_inline( 'complete the install', 'complete-the-install' ) )
6404 ),
6405 'activation_pending',
6406 'Thanks!'
6407 );
6408 }
6409
6410 /**
6411 * Check if currently in plugin activation.
6412 *
6413 * @author Vova Feldman (@svovaf)
6414 * @since 1.1.4
6415 *
6416 * @return bool
6417 */
6418 function is_plugin_activation() {
6419 $result = get_transient( "fs_{$this->_module_type}_{$this->_slug}_activated" );
6420
6421 return !empty($result);
6422 }
6423
6424 /**
6425 *
6426 * NOTE: admin_menu action executed before admin_init.
6427 *
6428 * @author Vova Feldman (@svovaf)
6429 * @since 1.0.7
6430 */
6431 function _admin_init_action() {
6432 /**
6433 * Automatically redirect to connect/activation page after plugin activation.
6434 *
6435 * @since 1.1.7 Do NOT redirect to opt-in when running in network admin mode.
6436 */
6437 if ( $this->is_plugin_activation() ) {
6438 delete_transient( "fs_{$this->_module_type}_{$this->_slug}_activated" );
6439
6440 if ( isset( $_GET['activate-multi'] ) ) {
6441 /**
6442 * Don't redirect if activating multiple plugins at once (bulk activation).
6443 */
6444 } else {
6445 $this->_redirect_on_activation_hook();
6446 return;
6447 }
6448 }
6449
6450 if ( fs_request_is_action( $this->get_unique_affix() . '_skip_activation' ) ) {
6451 check_admin_referer( $this->get_unique_affix() . '_skip_activation' );
6452
6453 $this->skip_connection( null, fs_is_network_admin() );
6454
6455 fs_redirect( $this->get_after_activation_url( 'after_skip_url' ) );
6456 }
6457
6458 if ( $this->is_network_activation_mode() &&
6459 fs_request_is_action( $this->get_unique_affix() . '_delegate_activation' )
6460 ) {
6461 check_admin_referer( $this->get_unique_affix() . '_delegate_activation' );
6462
6463 $this->delegate_connection();
6464
6465 fs_redirect( $this->get_after_activation_url( 'after_delegation_url' ) );
6466 }
6467
6468 $this->_add_upgrade_action_link();
6469
6470 if ( ! $this->is_addon() &&
6471 ! ( ! $this->_is_network_active && fs_is_network_admin() ) &&
6472 (
6473 ( true === $this->_storage->require_license_activation ) ||
6474 // Not registered nor anonymous.
6475 ( ! $this->is_registered() && ! $this->is_anonymous() ) ||
6476 // OR, network level and in network upgrade mode.
6477 ( fs_is_network_admin() && $this->_is_network_active && $this->is_network_upgrade_mode() )
6478 )
6479 ) {
6480 if ( ! $this->is_pending_activation() ) {
6481 if ( ! $this->_menu->is_main_settings_page() ) {
6482 /**
6483 * If a user visits any other admin page before activating the premium-only theme with a valid
6484 * license, reactivate the previous theme.
6485 *
6486 * @author Leo Fajardo (@leorw)
6487 * @since 1.2.2
6488 */
6489 if ( $this->is_theme()
6490 && ! $this->has_settings_menu()
6491 && ! isset( $_REQUEST['fs_action'] )
6492 && $this->can_activate_previous_theme()
6493 ) {
6494 if ( $this->is_only_premium() ) {
6495 $this->activate_previous_theme();
6496 return;
6497 }
6498
6499 if ( true === $this->_storage->require_license_activation ) {
6500 $this->_storage->require_license_activation = false;
6501 }
6502 }
6503
6504 if ( ! fs_is_network_admin() &&
6505 $this->is_network_activation_mode() &&
6506 ! $this->is_delegated_connection()
6507 ) {
6508 return;
6509 }
6510
6511 if ( $this->is_plugin_new_install() || $this->is_only_premium() ) {
6512 if ( ! $this->_anonymous_mode ) {
6513 // Show notice for new plugin installations.
6514 $this->_admin_notices->add(
6515 sprintf(
6516 $this->get_text_inline( 'You are just one step away - %s', 'you-are-step-away' ),
6517 sprintf( '<b><a href="%s">%s</a></b>',
6518 $this->get_activation_url( array(), ! $this->is_delegated_connection() ),
6519 sprintf( $this->get_text_x_inline( 'Complete "%s" Activation Now',
6520 '%s - plugin name. As complete "PluginX" activation now', 'activate-x-now' ), $this->get_plugin_name() )
6521 )
6522 ),
6523 '',
6524 'update-nag'
6525 );
6526 }
6527 } else {
6528 if ( $this->should_add_sticky_optin_notice() ) {
6529 $this->add_sticky_optin_admin_notice();
6530 }
6531
6532 if ( $this->has_filter( 'optin_pointer_element' ) ) {
6533 // Don't show admin nag if plugin update.
6534 wp_enqueue_script( 'wp-pointer' );
6535 wp_enqueue_style( 'wp-pointer' );
6536
6537 $this->_enqueue_connect_essentials();
6538
6539 add_action( 'admin_print_footer_scripts', array(
6540 $this,
6541 '_add_connect_pointer_script'
6542 ) );
6543 }
6544 }
6545 }
6546 }
6547
6548 if ( $this->is_theme() &&
6549 $this->_menu->is_main_settings_page()
6550 ) {
6551 $this->_show_theme_activation_optin_dialog();
6552 }
6553 }
6554 }
6555
6556 /**
6557 * @author Vova Feldman (@svovaf)
6558 * @since 2.0.0
6559 *
6560 * @return bool
6561 */
6562 private function should_add_sticky_optin_notice() {
6563 if ( fs_is_network_admin() ) {
6564 if ( ! $this->_is_network_active ) {
6565 return false;
6566 }
6567
6568 if ( ! $this->is_network_activation_mode() ) {
6569 return false;
6570 }
6571
6572 return ! isset( $this->_storage->sticky_optin_added_ms );
6573 }
6574
6575 if ( ! $this->is_activation_mode() ) {
6576 return false;
6577 }
6578
6579 // If running from a blog admin and delegated the connection.
6580 return ! isset( $this->_storage->sticky_optin_added );
6581 }
6582
6583 /**
6584 * @author Leo Fajardo (@leorw)
6585 * @since 2.0.0
6586 */
6587 private function add_sticky_optin_admin_notice() {
6588 if ( ! $this->_is_network_active || ! fs_is_network_admin() ) {
6589 $this->_storage->sticky_optin_added = true;
6590 } else {
6591 $this->_storage->sticky_optin_added_ms = true;
6592 }
6593
6594 // Show notice for new plugin installations.
6595 $this->_admin_notices->add_sticky(
6596 sprintf(
6597 $this->get_text_inline( 'We made a few tweaks to the %s, %s', 'few-plugin-tweaks' ),
6598 $this->_module_type,
6599 sprintf( '<b><a href="%s">%s</a></b>',
6600 $this->get_activation_url(),
6601 sprintf( $this->get_text_inline( 'Opt in to make "%s" better!', 'optin-x-now' ), $this->get_plugin_name() )
6602 )
6603 ),
6604 'connect_account',
6605 '',
6606 'update-nag'
6607 );
6608 }
6609
6610 /**
6611 * Enqueue connect requires scripts and styles.
6612 *
6613 * @author Vova Feldman (@svovaf)
6614 * @since 1.1.4
6615 */
6616 function _enqueue_connect_essentials() {
6617 wp_enqueue_script( 'jquery' );
6618 wp_enqueue_script( 'json2' );
6619
6620 fs_enqueue_local_script( 'postmessage', 'nojquery.ba-postmessage.min.js' );
6621 fs_enqueue_local_script( 'fs-postmessage', 'postmessage.js' );
6622
6623 fs_enqueue_local_style( 'fs_connect', '/admin/connect.css' );
6624 }
6625
6626 /**
6627 * Add connect / opt-in pointer.
6628 *
6629 * @author Vova Feldman (@svovaf)
6630 * @since 1.1.4
6631 */
6632 function _add_connect_pointer_script() {
6633 $vars = array( 'id' => $this->_module_id );
6634 $pointer_content = fs_get_template( 'connect.php', $vars );
6635 ?>
6636 <script type="text/javascript">// <![CDATA[
6637 jQuery(document).ready(function ($) {
6638 if ('undefined' !== typeof(jQuery().pointer)) {
6639
6640 var element = <?php echo $this->apply_filters( 'optin_pointer_element', '$("#non_existing_element");' ) ?>;
6641
6642 if (element.length > 0) {
6643 var optin = $(element).pointer($.extend(true, {}, {
6644 content : <?php echo json_encode( $pointer_content ) ?>,
6645 position : {
6646 edge : 'left',
6647 align: 'center'
6648 },
6649 buttons : function () {
6650 // Don't show pointer buttons.
6651 return '';
6652 },
6653 pointerWidth: 482
6654 }, <?php echo $this->apply_filters( 'optin_pointer_options_json', '{}' ) ?>));
6655
6656 <?php
6657 echo $this->apply_filters( 'optin_pointer_execute', "
6658
6659 optin.pointer('open');
6660
6661 // Tag the opt-in pointer with custom class.
6662 $('.wp-pointer #fs_connect')
6663 .parents('.wp-pointer.wp-pointer-top')
6664 .addClass('fs-opt-in-pointer');
6665
6666 ", 'element', 'optin' ) ?>
6667 }
6668 }
6669 });
6670 // ]]></script>
6671 <?php
6672 }
6673
6674 /**
6675 * Return current page's URL.
6676 *
6677 * @author Vova Feldman (@svovaf)
6678 * @since 1.0.7
6679 *
6680 * @return string
6681 */
6682 function current_page_url() {
6683 $url = 'http';
6684
6685 if ( isset( $_SERVER["HTTPS"] ) ) {
6686 if ( $_SERVER["HTTPS"] == "on" ) {
6687 $url .= "s";
6688 }
6689 }
6690 $url .= "://";
6691 if ( $_SERVER["SERVER_PORT"] != "80" ) {
6692 $url .= $_SERVER["SERVER_NAME"] . ":" . $_SERVER["SERVER_PORT"] . $_SERVER["REQUEST_URI"];
6693 } else {
6694 $url .= $_SERVER["SERVER_NAME"] . $_SERVER["REQUEST_URI"];
6695 }
6696
6697 return esc_url( $url );
6698 }
6699
6700 /**
6701 * Check if the current page is the plugin's main admin settings page.
6702 *
6703 * @author Vova Feldman (@svovaf)
6704 * @since 1.0.7
6705 *
6706 * @return bool
6707 */
6708 function _is_plugin_page() {
6709 return fs_is_plugin_page( $this->_menu->get_raw_slug() ) ||
6710 fs_is_plugin_page( $this->_slug );
6711 }
6712
6713 /* Events
6714 ------------------------------------------------------------------------------------------------------------------*/
6715 /**
6716 * Delete site install from Database.
6717 *
6718 * @author Vova Feldman (@svovaf)
6719 * @since 1.0.1
6720 *
6721 * @param bool $store
6722 * @param int|null $blog_id Since 2.0.0
6723 *
6724 * @return false|int The install ID if deleted. Otherwise, FALSE (when install not exist).
6725 */
6726 function _delete_site( $store = true, $blog_id = null ) {
6727 return self::_delete_site_by_slug( $this->_slug, $this->_module_type, $store, $blog_id );
6728 }
6729
6730 /**
6731 * Delete site install from Database.
6732 *
6733 * @author Vova Feldman (@svovaf)
6734 * @since 1.2.2.7
6735 *
6736 * @param string $slug
6737 * @param string $module_type
6738 * @param bool $store
6739 * @param int|null $blog_id Since 2.0.0
6740 *
6741 * @return false|int The install ID if deleted. Otherwise, FALSE (when install not exist).
6742 */
6743 static function _delete_site_by_slug( $slug, $module_type, $store = true, $blog_id = null ) {
6744 $sites = self::get_all_sites( $module_type, $blog_id );
6745
6746 $install_id = false;
6747
6748 if ( isset( $sites[ $slug ] ) ) {
6749 if ( is_object( $sites[ $slug ] ) ) {
6750 $install_id = $sites[ $slug ]->id;
6751 }
6752
6753 unset( $sites[ $slug ] );
6754
6755 self::set_account_option_by_module( $module_type, 'sites', $sites, $store, $blog_id );
6756 }
6757
6758 return $install_id;
6759 }
6760
6761 /**
6762 * Delete user.
6763 *
6764 * @author Vova Feldman (@svovaf)
6765 * @since 2.0.0
6766 *
6767 * @param number $user_id
6768 * @param bool $store
6769 *
6770 * @return false|int The user ID if deleted. Otherwise, FALSE (when install not exist).
6771 */
6772 private static function delete_user( $user_id, $store = true ) {
6773 $users = self::get_all_users();
6774
6775 if ( ! is_array( $users ) || ! isset( $users[ $user_id ] ) ) {
6776 return false;
6777 }
6778
6779 unset( $users[ $user_id ] );
6780
6781 self::$_accounts->set_option( 'users', $users, $store );
6782
6783 return $user_id;
6784 }
6785
6786 /**
6787 * Delete plugin's plans information.
6788 *
6789 * @param bool $store Flush to Database if true.
6790 * @param bool $keep_associated_plans If set to false, delete all plans, even if a plan is associated with an install.
6791 *
6792 * @author Vova Feldman (@svovaf)
6793 * @since 1.0.9
6794 */
6795 private function _delete_plans( $store = true, $keep_associated_plans = true ) {
6796 $this->_logger->entrance();
6797
6798 $plans = self::get_all_plans( $this->_module_type );
6799
6800 $plans_to_keep = array();
6801
6802 if ( $keep_associated_plans ) {
6803 $plans_ids_to_keep = $this->get_plans_ids_associated_with_installs();
6804 foreach ( $plans_ids_to_keep as $plan_id ) {
6805 $plan = self::_get_plan_by_id( $plan_id );
6806 if ( is_object( $plan ) ) {
6807 $plans_to_keep[] = $plan;
6808 }
6809 }
6810 }
6811
6812 if ( ! empty( $plans_to_keep ) ) {
6813 $plans[ $this->_slug ] = $plans_to_keep;
6814 } else {
6815 unset( $plans[ $this->_slug ] );
6816 }
6817
6818 $this->set_account_option( 'plans', $plans, $store );
6819 }
6820
6821 /**
6822 * Delete all plugin licenses.
6823 *
6824 * @author Vova Feldman (@svovaf)
6825 * @since 1.0.9
6826 *
6827 * @param bool $store
6828 */
6829 private function _delete_licenses( $store = true ) {
6830 $this->_logger->entrance();
6831
6832 $all_licenses = self::get_all_licenses();
6833
6834 unset( $all_licenses[ $this->_module_id ] );
6835
6836 self::$_accounts->set_option( 'all_licenses', $all_licenses, $store );
6837 }
6838
6839 /**
6840 * Check if Freemius was added on new plugin installation.
6841 *
6842 * @author Vova Feldman (@svovaf)
6843 * @since 1.1.5
6844 *
6845 * @return bool
6846 */
6847 function is_plugin_new_install() {
6848 return isset( $this->_storage->is_plugin_new_install ) &&
6849 $this->_storage->is_plugin_new_install;
6850 }
6851
6852 /**
6853 * Check if it's the first plugin release that is running Freemius.
6854 *
6855 * @author Vova Feldman (@svovaf)
6856 * @since 1.2.1.5
6857 *
6858 * @return bool
6859 */
6860 function is_first_freemius_powered_version() {
6861 return empty( $this->_storage->plugin_last_version );
6862 }
6863
6864 /**
6865 * @author Leo Fajardo (@leorw)
6866 * @since 1.2.2
6867 *
6868 * @return bool|string
6869 */
6870 private function get_previous_theme_slug() {
6871 return isset( $this->_storage->previous_theme ) ?
6872 $this->_storage->previous_theme :
6873 false;
6874 }
6875
6876 /**
6877 * @author Leo Fajardo (@leorw)
6878 * @since 1.2.2
6879 *
6880 * @return string
6881 */
6882 private function can_activate_previous_theme() {
6883 $slug = $this->get_previous_theme_slug();
6884 if ( false !== $slug && current_user_can( 'switch_themes' ) ) {
6885 $theme_instance = wp_get_theme( $slug );
6886
6887 return $theme_instance->exists();
6888 }
6889
6890 return false;
6891 }
6892
6893 /**
6894 * @author Leo Fajardo (@leorw)
6895 * @since 1.2.2
6896 */
6897 private function activate_previous_theme() {
6898 switch_theme( $this->get_previous_theme_slug() );
6899 unset( $this->_storage->previous_theme );
6900
6901 global $pagenow;
6902 if ( 'themes.php' === $pagenow ) {
6903 /**
6904 * Refresh the active theme information.
6905 *
6906 * @author Leo Fajardo (@leorw)
6907 * @since 1.2.2
6908 */
6909 fs_redirect( $this->admin_url( $pagenow ) );
6910 }
6911 }
6912
6913 /**
6914 * @author Leo Fajardo (@leorw)
6915 * @since 1.2.2
6916 *
6917 * @return string
6918 */
6919 function get_previous_theme_activation_url() {
6920 if ( ! $this->can_activate_previous_theme() ) {
6921 return '';
6922 }
6923
6924 /**
6925 * Activation URL
6926 *
6927 * @author Leo Fajardo (@leorw)
6928 * @since 1.2.2
6929 */
6930 return wp_nonce_url(
6931 $this->admin_url( 'themes.php?action=activate&stylesheet=' . urlencode( $this->get_previous_theme_slug() ) ),
6932 'switch-theme_' . $this->get_previous_theme_slug()
6933 );
6934 }
6935
6936 /**
6937 * Saves the slug of the previous theme if it still exists so that it can be used by the logic in the opt-in
6938 * form that decides whether to add a close button to the opt-in dialog or not. So after a premium-only theme is
6939 * activated, the close button will appear and will reactivate the previous theme if clicked. If the previous
6940 * theme doesn't exist, then there will be no close button.
6941 *
6942 * @author Leo Fajardo (@leorw)
6943 * @since 1.2.2
6944 *
6945 * @param string $slug_or_name Old theme's slug or name.
6946 * @param bool|WP_Theme $old_theme WP_Theme instance of the old theme if it still exists.
6947 */
6948 function _activate_theme_event_hook( $slug_or_name, $old_theme = false ) {
6949 $this->_storage->previous_theme = ( false !== $old_theme ) ?
6950 $old_theme->get_stylesheet() :
6951 $slug_or_name;
6952
6953 $this->_activate_plugin_event_hook();
6954 }
6955
6956 /**
6957 * Plugin activated hook.
6958 *
6959 * @author Vova Feldman (@svovaf)
6960 * @since 1.0.1
6961 *
6962 * @uses FS_Api
6963 */
6964 function _activate_plugin_event_hook() {
6965 $this->_logger->entrance( 'slug = ' . $this->_slug );
6966
6967 if ( ! $this->is_user_admin() ) {
6968 return;
6969 }
6970
6971 $this->unregister_uninstall_hook();
6972
6973 // Clear API cache on activation.
6974 FS_Api::clear_cache();
6975
6976 $is_premium_version_activation = ( current_filter() !== ( 'activate_' . $this->_free_plugin_basename ) );
6977
6978 $this->_logger->info( 'Activating ' . ( $is_premium_version_activation ? 'premium' : 'free' ) . ' plugin version.' );
6979
6980 // 1. If running in the activation of the FREE module, get the basename of the PREMIUM.
6981 // 2. If running in the activation of the PREMIUM module, get the basename of the FREE.
6982 $other_version_basename = $is_premium_version_activation ?
6983 $this->_free_plugin_basename :
6984 $this->premium_plugin_basename();
6985
6986 if ( ! $this->_is_network_active ) {
6987 /**
6988 * During the activation, the plugin isn't yet active, therefore,
6989 * _is_network_active will be set to false even if it's a network level
6990 * activation. So we need to fix that by looking at the is_network_admin() value.
6991 *
6992 * @author Vova Feldman
6993 */
6994 $this->_is_network_active = (
6995 $this->_is_multisite_integrated &&
6996 // Themes are always network activated, but the ACTUAL activation is per site.
6997 $this->is_plugin() &&
6998 fs_is_network_admin()
6999 );
7000 }
7001
7002 /**
7003 * If the other module version is activate, deactivate it.
7004 *
7005 * is_plugin_active() checks if the plugin active on the site or the network level
7006 * and deactivate_plugins() deactivates the plugin whether its activated on the site
7007 * or network level.
7008 *
7009 * @author Leo Fajardo (@leorw)
7010 * @since 1.2.2
7011 */
7012 if ( is_plugin_active( $other_version_basename ) ) {
7013 deactivate_plugins( $other_version_basename );
7014 }
7015
7016 if ( $this->is_registered() ) {
7017 if ( $is_premium_version_activation ) {
7018 $this->reconnect_locally();
7019 }
7020
7021
7022 // Schedule re-activation event and sync.
7023 // $this->sync_install( array(), true );
7024 $this->schedule_install_sync();
7025
7026 // If activating the premium module version, add an admin notice to congratulate for an upgrade completion.
7027 if ( $is_premium_version_activation ) {
7028 $this->_admin_notices->add(
7029 sprintf( $this->get_text_inline( 'The upgrade of %s was successfully completed.', 'successful-version-upgrade-message' ), sprintf( '<b>%s</b>', $this->_plugin->title ) ),
7030 $this->get_text_x_inline( 'W00t',
7031 'Used to express elation, enthusiasm, or triumph (especially in electronic communication).', 'woot' ) . '!'
7032 );
7033 }
7034 } else if ( $this->is_anonymous() ) {
7035 if ( isset( $this->_storage->is_anonymous_ms ) && $this->_storage->is_anonymous_ms['is'] ) {
7036 $plugin_version = $this->_storage->is_anonymous_ms['version'];
7037 $network = true;
7038 } else {
7039 $plugin_version = $this->_storage->is_anonymous['version'];
7040 $network = false;
7041 }
7042
7043 /**
7044 * Reset "skipped" click cache on the following:
7045 * 1. Freemius DEV mode.
7046 * 2. WordPress DEBUG mode.
7047 * 3. If a plugin and the user skipped the exact same version before.
7048 *
7049 * @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).
7050 *
7051 * @todo 4. If explicitly asked to retry after every activation.
7052 */
7053 if ( WP_FS__DEV_MODE ||
7054 (
7055 ( $this->is_plugin() || ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ) &&
7056 $this->get_plugin_version() == $plugin_version
7057 )
7058 ) {
7059 $this->reset_anonymous_mode( $network );
7060 }
7061 }
7062
7063 if (
7064 $is_premium_version_activation &&
7065 (
7066 ( ! $this->is_registered() && $this->is_anonymous() ) ||
7067 (
7068 $this->is_registered() &&
7069 ! $this->is_trial() &&
7070 ! $this->has_features_enabled_license()
7071 )
7072 )
7073 ) {
7074 $this->_storage->require_license_activation = true;
7075 }
7076
7077 if ( ! isset( $this->_storage->is_plugin_new_install ) ) {
7078 /**
7079 * If no previous version of plugin's version exist, it means that it's either
7080 * the first time that the plugin installed on the site, or the plugin was installed
7081 * before but didn't have Freemius integrated.
7082 *
7083 * Since register_activation_hook() do NOT fires on updates since 3.1, and only fires
7084 * on manual activation via the dashboard, is_plugin_activation() is TRUE
7085 * only after immediate activation.
7086 *
7087 * @since 1.1.4
7088 * @link https://make.wordpress.org/core/2010/10/27/plugin-activation-hooks-no-longer-fire-for-updates/
7089 */
7090 $this->_storage->is_plugin_new_install = empty( $this->_storage->plugin_last_version );
7091 }
7092
7093 if ( ! $this->_anonymous_mode &&
7094 $this->has_api_connectivity( WP_FS__DEV_MODE ) &&
7095 ! $this->_isAutoInstall
7096 ) {
7097 // Store hint that the plugin was just activated to enable auto-redirection to settings.
7098 set_transient( "fs_{$this->_module_type}_{$this->_slug}_activated", true, 60 );
7099 }
7100
7101 /**
7102 * Activation hook is executed after the plugin's main file is loaded, therefore,
7103 * after the plugin was loaded. The logic is located at activate_plugin()
7104 * ./wp-admin/includes/plugin.php.
7105 *
7106 * @author Vova Feldman (@svovaf)
7107 * @since 1.1.9
7108 */
7109 $this->_storage->was_plugin_loaded = true;
7110 }
7111
7112 /**
7113 * Delete account.
7114 *
7115 * @author Vova Feldman (@svovaf)
7116 * @since 1.0.3
7117 *
7118 * @param bool $check_user Enforce checking if user have plugins activation privileges.
7119 */
7120 function delete_account_event( $check_user = true ) {
7121 $this->_logger->entrance( 'slug = ' . $this->_slug );
7122
7123 if ( $check_user && ! $this->is_user_admin() ) {
7124 return;
7125 }
7126
7127 $this->do_action( 'before_account_delete' );
7128
7129 // Clear all admin notices.
7130 $this->_admin_notices->clear_all_sticky( false );
7131
7132 $this->_delete_site( false );
7133
7134 $delete_network_common_data = true;
7135
7136 if ( $this->_is_network_active ) {
7137 $installs = $this->get_blog_install_map();
7138
7139 // Don't delete common network data unless no other installs left.
7140 $delete_network_common_data = empty( $installs );
7141 }
7142
7143 if ( $delete_network_common_data ) {
7144 $this->_delete_plans( false );
7145
7146 $this->_delete_licenses( false );
7147
7148 // Delete add-ons related to plugin's account.
7149 $this->_delete_account_addons( false );
7150 }
7151
7152 // @todo Delete plans and licenses of add-ons.
7153
7154 self::$_accounts->store();
7155
7156 /**
7157 * IMPORTANT:
7158 * Clear crons must be executed before clearing all storage.
7159 * Otherwise, the cron will not be cleared.
7160 */
7161 if ( $delete_network_common_data ) {
7162 $this->clear_sync_cron();
7163 }
7164
7165 $this->clear_install_sync_cron();
7166
7167 // Clear all storage data.
7168 $this->_storage->clear_all( true, array(
7169 'connectivity_test',
7170 'is_on',
7171 ), false );
7172
7173 // Send delete event.
7174 $this->get_api_site_scope()->call( '/', 'delete' );
7175
7176 $this->do_action( 'after_account_delete' );
7177 }
7178
7179 /**
7180 * Delete network level account.
7181 *
7182 * @author Vova Feldman (@svovaf)
7183 * @since 2.0.0
7184 *
7185 * @param bool $check_user Enforce checking if user have plugins activation privileges.
7186 */
7187 function delete_network_account_event( $check_user = true ) {
7188 $this->_logger->entrance( 'slug = ' . $this->_slug );
7189
7190 if ( $check_user && ! $this->is_user_admin() ) {
7191 return;
7192 }
7193
7194 $this->do_action( 'before_network_account_delete' );
7195
7196 // Clear all admin notices.
7197 $this->_admin_notices->clear_all_sticky();
7198
7199 $this->_delete_plans( false, false );
7200
7201 $this->_delete_licenses( false );
7202
7203 // Delete add-ons related to plugin's account.
7204 $this->_delete_account_addons( false );
7205
7206 // @todo Delete plans and licenses of add-ons.
7207
7208 self::$_accounts->store( true );
7209
7210 /**
7211 * IMPORTANT:
7212 * Clear crons must be executed before clearing all storage.
7213 * Otherwise, the cron will not be cleared.
7214 */
7215 $this->clear_sync_cron( true );
7216 $this->clear_install_sync_cron( true );
7217
7218 $sites = self::get_sites();
7219
7220 $install_ids = array();
7221 foreach ( $sites as $site ) {
7222 $blog_id = self::get_site_blog_id( $site );
7223
7224 $install_id = $this->_delete_site( true, $blog_id );
7225
7226 // Clear all storage data.
7227 $this->_storage->clear_all( true, array( 'connectivity_test' ), $blog_id );
7228
7229 if ( FS_Site::is_valid_id( $install_id ) ) {
7230 $install_ids[] = $install_id;
7231 }
7232
7233 switch_to_blog( $blog_id );
7234
7235 $this->do_action( 'after_account_delete' );
7236
7237 restore_current_blog();
7238 }
7239
7240 $this->_storage->clear_all( true, array(
7241 'connectivity_test',
7242 'is_on',
7243 ), true );
7244
7245 // Send delete event.
7246 if ( ! empty( $install_ids ) ) {
7247 $result = $this->get_current_or_network_user_api_scope()->call( "/plugins/{$this->_module_id}/installs.json?ids=" . implode( ',', $install_ids ), 'delete' );
7248 }
7249
7250 $this->do_action( 'after_network_account_delete' );
7251 }
7252
7253 /**
7254 * Plugin deactivation hook.
7255 *
7256 * @author Vova Feldman (@svovaf)
7257 * @since 1.0.1
7258 */
7259 function _deactivate_plugin_hook() {
7260 $this->_logger->entrance( 'slug = ' . $this->_slug );
7261
7262 if ( ! $this->is_user_admin() ) {
7263 return;
7264 }
7265
7266 $is_network_deactivation = fs_is_network_admin();
7267 $storage_keys_for_removal = array();
7268
7269 $this->_admin_notices->clear_all_sticky();
7270
7271 $storage_keys_for_removal[] = 'sticky_optin_added';
7272 if ( isset( $this->_storage->sticky_optin_added ) ) {
7273 unset( $this->_storage->sticky_optin_added );
7274 }
7275
7276 if ( ! isset( $this->_storage->is_plugin_new_install ) ) {
7277 // Remember that plugin was already installed.
7278 $this->_storage->is_plugin_new_install = false;
7279 }
7280
7281 // Hook to plugin uninstall.
7282 register_uninstall_hook( $this->_plugin_main_file_path, array( 'Freemius', '_uninstall_plugin_hook' ) );
7283
7284 $this->clear_module_main_file_cache();
7285 $this->clear_sync_cron( $this->_is_network_active );
7286 $this->clear_install_sync_cron();
7287
7288 if ( $this->is_registered() ) {
7289 if ( $this->is_premium() && ! $this->has_active_valid_license() ) {
7290 FS_Plugin_Updater::instance( $this )->delete_update_data();
7291 }
7292
7293 if ( $is_network_deactivation ) {
7294 // Send deactivation event.
7295 $this->sync_installs( array(
7296 'is_active' => false,
7297 ) );
7298 } else {
7299 // Send deactivation event.
7300 $this->sync_install( array(
7301 'is_active' => false,
7302 ) );
7303 }
7304 } else {
7305 if ( ! $this->has_api_connectivity() ) {
7306 // Reset connectivity test cache.
7307 unset( $this->_storage->connectivity_test );
7308
7309 $storage_keys_for_removal[] = 'connectivity_test';
7310 }
7311 }
7312
7313 if ( $is_network_deactivation ) {
7314 if ( isset( $this->_storage->sticky_optin_added_ms ) ) {
7315 unset( $this->_storage->sticky_optin_added_ms );
7316 }
7317
7318 if ( ! empty( $storage_keys_for_removal ) ) {
7319 $sites = self::get_sites();
7320
7321 foreach ( $sites as $site ) {
7322 $blog_id = self::get_site_blog_id( $site );
7323
7324 foreach ( $storage_keys_for_removal as $key ) {
7325 $this->_storage->remove( $key, false, $blog_id );
7326 }
7327
7328 $this->_storage->save( $blog_id );
7329 }
7330 }
7331 }
7332
7333 // Clear API cache on deactivation.
7334 FS_Api::clear_cache();
7335
7336 $this->remove_sdk_reference();
7337 }
7338
7339 /**
7340 * @author Vova Feldman (@svovaf)
7341 * @since 1.1.6
7342 */
7343 private function remove_sdk_reference() {
7344 global $fs_active_plugins;
7345
7346 foreach ( $fs_active_plugins->plugins as $sdk_path => $data ) {
7347 if ( $this->_plugin_basename == $data->plugin_path ) {
7348 unset( $fs_active_plugins->plugins[ $sdk_path ] );
7349 break;
7350 }
7351 }
7352
7353 fs_fallback_to_newest_active_sdk();
7354 }
7355
7356 /**
7357 * @author Vova Feldman (@svovaf)
7358 * @since 1.1.3
7359 *
7360 * @param bool $is_anonymous
7361 * @param bool|int $network_or_blog_id Since 2.0.0
7362 */
7363 private function set_anonymous_mode( $is_anonymous = true, $network_or_blog_id = 0 ) {
7364 // Store information regarding skip to try and opt-in the user
7365 // again in the future.
7366 $skip_info = array(
7367 'is' => $is_anonymous,
7368 'timestamp' => WP_FS__SCRIPT_START_TIME,
7369 'version' => $this->get_plugin_version(),
7370 );
7371
7372 if ( true === $network_or_blog_id ) {
7373 $this->_storage->is_anonymous_ms = $skip_info;
7374 } else {
7375 $this->_storage->store( 'is_anonymous', $skip_info, $network_or_blog_id );
7376 }
7377
7378 $this->network_upgrade_mode_completed();
7379
7380 // Update anonymous mode cache.
7381 $this->_is_anonymous = $is_anonymous;
7382 }
7383
7384 /**
7385 * @author Vova Feldman (@svovaf)
7386 * @since 2.0.0
7387 *
7388 * @param int $blog_id Site ID.
7389 * @param int $user_id User ID.
7390 * @param string $domain Site domain.
7391 * @param string $path Site path.
7392 * @param int $network_id Network ID. Only relevant on multi-network installations.
7393 * @param array $meta Metadata. Used to set initial site options.
7394 *
7395 * @uses Freemius::is_license_network_active() to check if the context license was network activated by the super-admin.
7396 * @uses Freemius::is_network_connected() to check if the super-admin network opted-in.
7397 * @uses Freemius::is_network_anonymous() to check if the super-admin network skipped.
7398 * @uses Freemius::is_network_delegated_connection() to check if the super-admin network delegated the connection to the site admins.
7399 */
7400 function _after_new_blog_callback( $blog_id, $user_id, $domain, $path, $network_id, $meta ) {
7401 $this->_logger->entrance();
7402
7403 if ( $this->is_premium() &&
7404 $this->is_network_connected() &&
7405 is_object( $this->_license ) &&
7406 $this->_license->can_activate( FS_Site::is_localhost_by_address( $domain ) ) &&
7407 $this->is_license_network_active( $blog_id )
7408 ) {
7409 /**
7410 * 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.
7411 */
7412 $current_blog_id = get_current_blog_id();
7413 $license = clone $this->_license;
7414
7415 $this->switch_to_blog( $blog_id );
7416
7417 // Opt-in with network user.
7418 $this->install_with_user(
7419 $this->get_network_user(),
7420 $license->secret_key,
7421 false,
7422 false,
7423 false
7424 );
7425
7426 if ( is_object( $this->_site ) ) {
7427 if ( $this->_site->license_id == $license->id ) {
7428 /**
7429 * If the license was activated successfully, sync the license data from the remote server.
7430 */
7431 $this->_license = $license;
7432 $this->sync_site_license();
7433 }
7434 }
7435
7436 $this->switch_to_blog( $current_blog_id );
7437
7438 if ( is_object( $this->_site ) ) {
7439 // Already connected (with or without a license), so no need to continue.
7440 return;
7441 }
7442 }
7443
7444 if ( $this->is_network_anonymous() ) {
7445 /**
7446 * Opt-in was network skipped so automatically skip the opt-in for the new site.
7447 */
7448 $this->skip_site_connection( $blog_id );
7449 } else if ( $this->is_network_delegated_connection() ) {
7450 /**
7451 * Opt-in was network delegated so automatically delegate the opt-in for the new site's admin.
7452 */
7453 $this->delegate_site_connection( $blog_id );
7454 } else if ( $this->is_network_connected() ) {
7455 /**
7456 * Opt-in was network activated so automatically opt-in with the network user and new site admin.
7457 */
7458 $current_blog_id = get_current_blog_id();
7459
7460 $this->switch_to_blog( $blog_id );
7461
7462 // Opt-in with network user.
7463 $this->install_with_user(
7464 $this->get_network_user(),
7465 false,
7466 false,
7467 false,
7468 false
7469 );
7470
7471 $this->switch_to_blog( $current_blog_id );
7472 } else {
7473 /**
7474 * If the super-admin mixed different options (connect, skip, delegated):
7475 * a) If at least one site connection was delegated, then automatically delegate connection.
7476 * 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.
7477 */
7478 $has_delegated_site = false;
7479
7480 $sites = self::get_sites();
7481 foreach ( $sites as $site ) {
7482 $blog_id = self::get_site_blog_id( $site );
7483
7484 if ( $this->is_site_delegated_connection( $blog_id ) ) {
7485 $has_delegated_site = true;
7486 break;
7487 }
7488 }
7489
7490 if ( $has_delegated_site ) {
7491 $this->delegate_site_connection( $blog_id );
7492 } else {
7493 $this->skip_site_connection( $blog_id );
7494 }
7495 }
7496 }
7497
7498 /**
7499 * @author Vova Feldman (@svovaf)
7500 * @since 1.1.3
7501 *
7502 * @param bool|int $network_or_blog_id Since 2.0.0.
7503 */
7504 private function reset_anonymous_mode( $network_or_blog_id = 0 ) {
7505 if ( true === $network_or_blog_id ) {
7506 unset( $this->_storage->is_anonymous_ms );
7507 } else {
7508 $this->_storage->remove( 'is_anonymous', true, $network_or_blog_id );
7509 }
7510
7511 /**
7512 * Ensure that this field is also "false", otherwise, if the current module's type is "theme" and the module
7513 * has no menus, the opt-in popup will not be shown immediately (in this case, the user will have to click
7514 * on the admin notice that contains the opt-in link in order to trigger the opt-in popup).
7515 *
7516 * @author Leo Fajardo (@leorw)
7517 * @since 1.2.2
7518 */
7519 if ( ! $this->_is_network_active ||
7520 0 === $network_or_blog_id ||
7521 get_current_blog_id() == $network_or_blog_id ||
7522 ( true === $network_or_blog_id && fs_is_network_admin() )
7523 ) {
7524 unset( $this->_is_anonymous );
7525 }
7526 }
7527
7528 /**
7529 * This is used to ensure that before redirecting to the opt-in page after resetting the anonymous mode or
7530 * deleting the account in the network level, the URL of the page to redirect to is correct.
7531 *
7532 * @author Leo Fajardo (@leorw)
7533 *
7534 * @since 2.1.3
7535 */
7536 private function maybe_set_slug_and_network_menu_exists_flag() {
7537 if ( ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
7538 $this->_menu->set_slug_and_network_menu_exists_flag( $this->_menu->has_menu() ?
7539 $this->_menu->get_slug() :
7540 $this->_slug
7541 );
7542 }
7543 }
7544
7545 /**
7546 * Clears the anonymous mode and redirects to the opt-in screen.
7547 *
7548 * @author Vova Feldman (@svovaf)
7549 * @since 1.1.7
7550 */
7551 function connect_again() {
7552 if ( ! $this->is_anonymous() ) {
7553 return;
7554 }
7555
7556 $this->reset_anonymous_mode( fs_is_network_admin() );
7557
7558 $this->maybe_set_slug_and_network_menu_exists_flag();
7559
7560 fs_redirect( $this->get_activation_url() );
7561 }
7562
7563 /**
7564 * Skip account connect, and set anonymous mode.
7565 *
7566 * @author Vova Feldman (@svovaf)
7567 * @since 1.1.1
7568 *
7569 * @param array|null $sites Since 2.0.0. Specific sites.
7570 * @param bool $skip_all_network Since 2.0.0. If true, skip connection for all sites.
7571 */
7572 function skip_connection( $sites = null, $skip_all_network = false ) {
7573 $this->_logger->entrance();
7574
7575 $this->_admin_notices->remove_sticky( 'connect_account' );
7576
7577 if ( $skip_all_network ) {
7578 $this->set_anonymous_mode( true, true );
7579 }
7580
7581 if ( ! $skip_all_network && empty( $sites ) ) {
7582 $this->skip_site_connection();
7583 } else {
7584 $uids = array();
7585
7586 if ( $skip_all_network ) {
7587 $this->set_anonymous_mode( true, true );
7588
7589 $sites = self::get_sites();
7590 foreach ( $sites as $site ) {
7591 $blog_id = self::get_site_blog_id( $site );
7592 $this->skip_site_connection( $blog_id, false );
7593 $uids[] = $this->get_anonymous_id( $blog_id );
7594 }
7595 } else if ( ! empty( $sites ) ) {
7596 foreach ( $sites as $site ) {
7597 $uids[] = $site['uid'];
7598 $this->skip_site_connection( $site['blog_id'], false );
7599 }
7600 }
7601
7602 // Send anonymous skip event.
7603 // No user identified info nor any tracking will be sent after the user skips the opt-in.
7604 $this->get_api_plugin_scope()->call( 'skip.json', 'put', array(
7605 'uids' => $uids,
7606 ) );
7607 }
7608
7609 $this->network_upgrade_mode_completed();
7610 }
7611
7612 /**
7613 * Skip connection for specific site in the network.
7614 *
7615 * @author Vova Feldman (@svovaf)
7616 * @since 2.0.0
7617 *
7618 * @param int|null $blog_id
7619 * @param bool $send_skip
7620 */
7621 private function skip_site_connection( $blog_id = null, $send_skip = true ) {
7622 $this->_logger->entrance();
7623
7624 $this->_admin_notices->remove_sticky( 'connect_account', $blog_id );
7625
7626 $this->set_anonymous_mode( true, $blog_id );
7627
7628 if ( $send_skip ) {
7629 $this->get_api_plugin_scope()->call( 'skip.json', 'put', array(
7630 'uids' => array( $this->get_anonymous_id( $blog_id ) ),
7631 ) );
7632 }
7633 }
7634
7635 /**
7636 * Plugin version update hook.
7637 *
7638 * @author Vova Feldman (@svovaf)
7639 * @since 1.0.4
7640 */
7641 private function update_plugin_version_event() {
7642 $this->_logger->entrance();
7643
7644 if ( ! $this->is_registered() ) {
7645 return;
7646 }
7647
7648 $this->schedule_install_sync();
7649 // $this->sync_install( array(), true );
7650 }
7651
7652 /**
7653 * Generate an MD5 signature of a plugins collection.
7654 * This helper methods used to identify changes in a plugins collection.
7655 *
7656 * @author Vova Feldman (@svovaf)
7657 * @since 2.0.0
7658 *
7659 * @param array [string]array $plugins
7660 *
7661 * @return string
7662 */
7663 private function get_plugins_thumbprint( $plugins ) {
7664 ksort( $plugins );
7665
7666 $thumbprint = '';
7667 foreach ( $plugins as $basename => $data ) {
7668 $thumbprint .= $data['slug'] . ',' .
7669 $data['Version'] . ',' .
7670 ( $data['is_active'] ? '1' : '0' ) . ';';
7671 }
7672
7673 return md5( $thumbprint );
7674 }
7675
7676 /**
7677 * Return a list of modified plugins since the last sync.
7678 *
7679 * Note:
7680 * There's no point to store a plugins counter since even if the number of
7681 * plugins didn't change, we still need to check if the versions are all the
7682 * same and the activity state is similar.
7683 *
7684 * @author Vova Feldman (@svovaf)
7685 * @since 1.1.8
7686 *
7687 * @return array|false
7688 */
7689 private function get_plugins_data_for_api() {
7690 // Alias.
7691 $site_active_plugins_option_name = 'active_plugins';
7692 $network_plugins_option_name = 'all_plugins';
7693
7694 /**
7695 * Collection of all site level active plugins.
7696 */
7697 $site_active_plugins_cache = self::$_accounts->get_option( $site_active_plugins_option_name );
7698
7699 if ( ! is_object( $site_active_plugins_cache ) ) {
7700 $site_active_plugins_cache = (object) array(
7701 'timestamp' => '',
7702 'md5' => '',
7703 'plugins' => array(),
7704 );
7705 }
7706
7707 $time = time();
7708
7709 if ( ! empty( $site_active_plugins_cache->timestamp ) &&
7710 ( $time - $site_active_plugins_cache->timestamp ) < WP_FS__TIME_5_MIN_IN_SEC
7711 ) {
7712 // Don't send plugin updates if last update was in the past 5 min.
7713 return false;
7714 }
7715
7716 // Write timestamp to lock the logic.
7717 $site_active_plugins_cache->timestamp = $time;
7718 self::$_accounts->set_option( $site_active_plugins_option_name, $site_active_plugins_cache, true );
7719
7720 // Reload options from DB.
7721 self::$_accounts->load( true );
7722 $site_active_plugins_cache = self::$_accounts->get_option( $site_active_plugins_option_name );
7723
7724 if ( $time != $site_active_plugins_cache->timestamp ) {
7725 // If timestamp is different, then another thread captured the lock.
7726 return false;
7727 }
7728
7729 /**
7730 * Collection of all plugins (network level).
7731 */
7732 $network_plugins_cache = self::$_accounts->get_option( $network_plugins_option_name );
7733
7734 if ( ! is_object( $network_plugins_cache ) ) {
7735 $network_plugins_cache = (object) array(
7736 'timestamp' => '',
7737 'md5' => '',
7738 'plugins' => array(),
7739 );
7740 }
7741
7742 // Check if there's a change in plugins.
7743 $network_plugins = self::get_network_plugins();
7744 $site_active_plugins = self::get_site_active_plugins();
7745
7746 $network_plugins_thumbprint = $this->get_plugins_thumbprint( $network_plugins );
7747 $site_active_plugins_thumbprint = $this->get_plugins_thumbprint( $site_active_plugins );
7748
7749 // Check if plugins status changed (version or active/inactive).
7750 $network_plugins_changed = ( $network_plugins_cache->md5 !== $network_plugins_thumbprint );
7751 $site_active_plugins_changed = ( $site_active_plugins_cache->md5 !== $site_active_plugins_thumbprint );
7752
7753 if ( ! $network_plugins_changed &&
7754 ! $site_active_plugins_changed
7755 ) {
7756 // No changes.
7757 return array();
7758 }
7759
7760 $plugins_update_data = array();
7761
7762 foreach ( $network_plugins_cache->plugins as $basename => $data ) {
7763 if ( ! isset( $network_plugins[ $basename ] ) ) {
7764 // Plugin uninstalled.
7765 $uninstalled_plugin_data = $data;
7766 $uninstalled_plugin_data['is_active'] = false;
7767 $uninstalled_plugin_data['is_uninstalled'] = true;
7768 $plugins_update_data[] = $uninstalled_plugin_data;
7769
7770 unset( $network_plugins[ $basename ] );
7771
7772 unset( $network_plugins_cache->plugins[ $basename ] );
7773 unset( $site_active_plugins_cache->plugins[ $basename ] );
7774
7775 continue;
7776 }
7777
7778 $was_active = $data['is_active'] ||
7779 ( isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
7780 true === $site_active_plugins_cache->plugins[ $basename ]['is_active'] );
7781 $is_active = $network_plugins[ $basename ]['is_active'] ||
7782 ( isset( $site_active_plugins[ $basename ] ) &&
7783 $site_active_plugins[ $basename ]['is_active'] );
7784
7785 if ( ! isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
7786 isset( $site_active_plugins[ $basename ] )
7787 ) {
7788 // Plugin was site level activated.
7789 $site_active_plugins_cache->plugins[ $basename ] = $network_plugins[ $basename ];
7790 $site_active_plugins_cache->plugins[ $basename ]['is_active'] = true;
7791 } else if ( isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
7792 ! isset( $site_active_plugins[ $basename ] )
7793 ) {
7794 // Plugin was site level deactivated.
7795 unset( $site_active_plugins_cache->plugins[ $basename ] );
7796 }
7797
7798 $prev_version = $data['version'];
7799 $current_version = $network_plugins[ $basename ]['Version'];
7800
7801 if ( $was_active !== $is_active || $prev_version !== $current_version ) {
7802 // Plugin activated or deactivated, or version changed.
7803
7804 if ( $was_active !== $is_active ) {
7805 if ( $data['is_active'] != $network_plugins[ $basename ]['is_active'] ) {
7806 $network_plugins_cache->plugins[ $basename ]['is_active'] = $data['is_active'];
7807 }
7808 }
7809
7810 if ( $prev_version !== $current_version ) {
7811 $network_plugins_cache->plugins[ $basename ]['Version'] = $current_version;
7812 }
7813
7814 $updated_plugin_data = $data;
7815 $updated_plugin_data['is_active'] = $is_active;
7816 $updated_plugin_data['version'] = $current_version;
7817 $updated_plugin_data['title'] = $network_plugins[ $basename ]['Name'];
7818 $plugins_update_data[] = $updated_plugin_data;
7819 }
7820 }
7821
7822 // Find new plugins that weren't yet seen before.
7823 foreach ( $network_plugins as $basename => $data ) {
7824 if ( ! isset( $network_plugins_cache->plugins[ $basename ] ) ) {
7825 // New plugin.
7826 $new_plugin = array(
7827 'slug' => $data['slug'],
7828 'version' => $data['Version'],
7829 'title' => $data['Name'],
7830 'is_active' => $data['is_active'],
7831 'is_uninstalled' => false,
7832 );
7833
7834 $plugins_update_data[] = $new_plugin;
7835 $network_plugins_cache->plugins[ $basename ] = $new_plugin;
7836
7837 if ( isset( $site_active_plugins[ $basename ] ) ) {
7838 $site_active_plugins_cache->plugins[ $basename ] = $new_plugin;
7839 $site_active_plugins_cache->plugins[ $basename ]['is_active'] = true;
7840 }
7841 }
7842 }
7843
7844 $site_active_plugins_cache->md5 = $site_active_plugins_thumbprint;
7845 $site_active_plugins_cache->timestamp = $time;
7846 self::$_accounts->set_option( $site_active_plugins_option_name, $site_active_plugins_cache, true );
7847
7848 $network_plugins_cache->md5 = $network_plugins_thumbprint;
7849 $network_plugins_cache->timestamp = $time;
7850 self::$_accounts->set_option( $network_plugins_option_name, $network_plugins_cache, true );
7851
7852 return $plugins_update_data;
7853 }
7854
7855 /**
7856 * Return a list of modified themes since the last sync.
7857 *
7858 * Note:
7859 * There's no point to store a themes counter since even if the number of
7860 * themes didn't change, we still need to check if the versions are all the
7861 * same and the activity state is similar.
7862 *
7863 * @author Vova Feldman (@svovaf)
7864 * @since 1.1.8
7865 *
7866 * @return array|false
7867 */
7868 private function get_themes_data_for_api() {
7869 // Alias.
7870 $option_name = 'all_themes';
7871
7872 $all_cached_themes = self::$_accounts->get_option( $option_name );
7873
7874 if ( ! is_object( $all_cached_themes ) ) {
7875 $all_cached_themes = (object) array(
7876 'timestamp' => '',
7877 'md5' => '',
7878 'themes' => array(),
7879 );
7880 }
7881
7882 $time = time();
7883
7884 if ( ! empty( $all_cached_themes->timestamp ) &&
7885 ( $time - $all_cached_themes->timestamp ) < WP_FS__TIME_5_MIN_IN_SEC
7886 ) {
7887 // Don't send theme updates if last update was in the past 5 min.
7888 return false;
7889 }
7890
7891 // Write timestamp to lock the logic.
7892 $all_cached_themes->timestamp = $time;
7893 self::$_accounts->set_option( $option_name, $all_cached_themes, true );
7894
7895 // Reload options from DB.
7896 self::$_accounts->load( true );
7897 $all_cached_themes = self::$_accounts->get_option( $option_name );
7898
7899 if ( $time != $all_cached_themes->timestamp ) {
7900 // If timestamp is different, then another thread captured the lock.
7901 return false;
7902 }
7903
7904 // Get active theme.
7905 $active_theme = wp_get_theme();
7906 $active_theme_stylesheet = $active_theme->get_stylesheet();
7907
7908 // Check if there's a change in themes.
7909 $all_themes = wp_get_themes();
7910
7911 // Check if themes changed.
7912 ksort( $all_themes );
7913
7914 $themes_signature = '';
7915 foreach ( $all_themes as $slug => $data ) {
7916 $is_active = ( $slug === $active_theme_stylesheet );
7917 $themes_signature .= $slug . ',' .
7918 $data->version . ',' .
7919 ( $is_active ? '1' : '0' ) . ';';
7920 }
7921
7922 // Check if themes status changed (version or active/inactive).
7923 $themes_changed = ( $all_cached_themes->md5 !== md5( $themes_signature ) );
7924
7925 $themes_update_data = array();
7926
7927 if ( $themes_changed ) {
7928 // Change in themes, report changes.
7929
7930 // Update existing themes info.
7931 foreach ( $all_cached_themes->themes as $slug => $data ) {
7932 $is_active = ( $slug === $active_theme_stylesheet );
7933
7934 if ( ! isset( $all_themes[ $slug ] ) ) {
7935 // Plugin uninstalled.
7936 $uninstalled_theme_data = $data;
7937 $uninstalled_theme_data['is_active'] = false;
7938 $uninstalled_theme_data['is_uninstalled'] = true;
7939 $themes_update_data[] = $uninstalled_theme_data;
7940
7941 unset( $all_themes[ $slug ] );
7942 unset( $all_cached_themes->themes[ $slug ] );
7943 } else if ( $data['is_active'] !== $is_active ||
7944 $data['version'] !== $all_themes[ $slug ]->version
7945 ) {
7946 // Plugin activated or deactivated, or version changed.
7947
7948 $all_cached_themes->themes[ $slug ]['is_active'] = $is_active;
7949 $all_cached_themes->themes[ $slug ]['version'] = $all_themes[ $slug ]->version;
7950
7951 $themes_update_data[] = $all_cached_themes->themes[ $slug ];
7952 }
7953 }
7954
7955 // Find new themes that weren't yet seen before.
7956 foreach ( $all_themes as $slug => $data ) {
7957 if ( ! isset( $all_cached_themes->themes[ $slug ] ) ) {
7958 $is_active = ( $slug === $active_theme_stylesheet );
7959
7960 // New plugin.
7961 $new_plugin = array(
7962 'slug' => $slug,
7963 'version' => $data->version,
7964 'title' => $data->name,
7965 'is_active' => $is_active,
7966 'is_uninstalled' => false,
7967 );
7968
7969 $themes_update_data[] = $new_plugin;
7970 $all_cached_themes->themes[ $slug ] = $new_plugin;
7971 }
7972 }
7973
7974 $all_cached_themes->md5 = md5( $themes_signature );
7975 $all_cached_themes->timestamp = time();
7976 self::$_accounts->set_option( $option_name, $all_cached_themes, true );
7977 }
7978
7979 return $themes_update_data;
7980 }
7981
7982 /**
7983 * Get site data for API install request.
7984 *
7985 * @author Vova Feldman (@svovaf)
7986 * @since 1.1.2
7987 *
7988 * @param string[] string $override
7989 * @param bool $include_plugins Since 1.1.8 by default include plugin changes.
7990 * @param bool $include_themes Since 1.1.8 by default include plugin changes.
7991 *
7992 * @return array
7993 */
7994 private function get_install_data_for_api(
7995 array $override,
7996 $include_plugins = true,
7997 $include_themes = true
7998 ) {
7999 if ( ! defined( 'WP_FS__TRACK_PLUGINS' ) || false !== WP_FS__TRACK_PLUGINS ) {
8000 /**
8001 * @since 1.1.8 Also send plugin updates.
8002 */
8003 if ( $include_plugins && ! isset( $override['plugins'] ) ) {
8004 $plugins = $this->get_plugins_data_for_api();
8005 if ( ! empty( $plugins ) ) {
8006 $override['plugins'] = $plugins;
8007 }
8008 }
8009 }
8010
8011 if ( ! defined( 'WP_FS__TRACK_THEMES' ) || false !== WP_FS__TRACK_THEMES ) {
8012 /**
8013 * @since 1.1.8 Also send themes updates.
8014 */
8015 if ( $include_themes && ! isset( $override['themes'] ) ) {
8016 $themes = $this->get_themes_data_for_api();
8017 if ( ! empty( $themes ) ) {
8018 $override['themes'] = $themes;
8019 }
8020 }
8021 }
8022
8023 $versions = $this->get_versions();
8024
8025 return array_merge( $versions, array(
8026 'version' => $this->get_plugin_version(),
8027 'is_premium' => $this->is_premium(),
8028 'language' => get_bloginfo( 'language' ),
8029 'charset' => get_bloginfo( 'charset' ),
8030 'title' => get_bloginfo( 'name' ),
8031 'url' => get_site_url(),
8032 // Special params.
8033 'is_active' => true,
8034 'is_disconnected' => $this->is_tracking_prohibited(),
8035 'is_uninstalled' => false,
8036 ), $override );
8037 }
8038
8039 /**
8040 * Update installs details.
8041 *
8042 * @todo V1 of multiste network support doesn't support plugin and theme data sending.
8043 *
8044 * @author Vova Feldman (@svovaf)
8045 * @since 2.0.0
8046 *
8047 * @param string[] string $override
8048 * @param bool $only_diff
8049 * @param bool $include_plugins Since 1.1.8 by default include plugin changes.
8050 * @param bool $include_themes Since 1.1.8 by default include plugin changes.
8051 *
8052 * @return array
8053 */
8054 private function get_installs_data_for_api(
8055 array $override,
8056 $only_diff = false,
8057 $include_plugins = true,
8058 $include_themes = true
8059 ) {
8060 /**
8061 * @since 1.1.8 Also send plugin updates.
8062 */
8063 // if ( $include_plugins && ! isset( $override['plugins'] ) ) {
8064 // $plugins = $this->get_plugins_data_for_api();
8065 // if ( ! empty( $plugins ) ) {
8066 // $override['plugins'] = $plugins;
8067 // }
8068 // }
8069 /**
8070 * @since 1.1.8 Also send themes updates.
8071 */
8072 // if ( $include_themes && ! isset( $override['themes'] ) ) {
8073 // $themes = $this->get_themes_data_for_api();
8074 // if ( ! empty( $themes ) ) {
8075 // $override['themes'] = $themes;
8076 // }
8077 // }
8078
8079 // Common properties.
8080 $versions = $this->get_versions();
8081 $common = array_merge( $versions, array(
8082 'version' => $this->get_plugin_version(),
8083 'is_premium' => $this->is_premium(),
8084 ), $override );
8085
8086
8087 $is_common_diff_for_any_site = false;
8088 $common_diff_union = array();
8089
8090 $installs_data = array();
8091
8092 $sites = self::get_sites();
8093
8094 foreach ( $sites as $site ) {
8095 $blog_id = self::get_site_blog_id( $site );
8096
8097 $install = $this->get_install_by_blog_id( $blog_id );
8098
8099 if ( is_object( $install ) ) {
8100 if ( $install->user_id != $this->_user->id ) {
8101 // Install belongs to a different owner.
8102 continue;
8103 }
8104
8105 if ( ! $this->is_premium() && $install->is_tracking_prohibited() ) {
8106 // Don't send updates regarding opted-out installs.
8107 continue;
8108 }
8109
8110 $install_data = $this->get_site_info( $site );
8111
8112 $uid = $install_data['uid'];
8113
8114 unset( $install_data['blog_id'] );
8115 unset( $install_data['uid'] );
8116
8117 $install_data['is_disconnected'] = $install->is_disconnected;
8118 $install_data['is_active'] = $this->is_active_for_site( $blog_id );
8119 $install_data['is_uninstalled'] = $install->is_uninstalled;
8120
8121 $common_diff = null;
8122 $is_common_diff = false;
8123 if ( $only_diff ) {
8124 $install_data = $this->get_install_diff_for_api( $install_data, $install, $override );
8125 $common_diff = $this->get_install_diff_for_api( $common, $install, $override );
8126
8127 $is_common_diff = ! empty( $common_diff );
8128
8129 if ( $is_common_diff ) {
8130 foreach ( $common_diff as $k => $v ) {
8131 if ( ! isset( $common_diff_union[ $k ] ) ) {
8132 $common_diff_union[ $k ] = $v;
8133 }
8134 }
8135 }
8136
8137 $is_common_diff_for_any_site = $is_common_diff_for_any_site || $is_common_diff;
8138 }
8139
8140 if ( ! empty( $install_data ) || $is_common_diff ) {
8141 // Add install ID and site unique ID.
8142 $install_data['id'] = $install->id;
8143 $install_data['uid'] = $uid;
8144
8145 $installs_data[] = $install_data;
8146 }
8147 }
8148 }
8149
8150 restore_current_blog();
8151
8152 if ( 0 < count( $installs_data ) && ( $is_common_diff_for_any_site || ! $only_diff ) ) {
8153 if ( ! $only_diff ) {
8154 $installs_data[] = $common;
8155 } else if ( ! empty( $common_diff_union ) ) {
8156 $installs_data[] = $common_diff_union;
8157 }
8158 }
8159
8160 foreach ( $installs_data as &$data ) {
8161 $data = (object) $data;
8162 }
8163
8164 return $installs_data;
8165 }
8166
8167 /**
8168 * Compare site actual data to the stored install data and return the differences for an API data sync.
8169 *
8170 * @author Vova Feldman (@svovaf)
8171 * @since 2.0.0
8172 *
8173 * @param array $site
8174 * @param FS_Site $install
8175 * @param string[] string $override
8176 *
8177 * @return array
8178 */
8179 private function get_install_diff_for_api( $site, $install, $override = array() ) {
8180 $diff = array();
8181 $special = array();
8182 $special_override = false;
8183
8184 foreach ( $site as $p => $v ) {
8185 if ( property_exists( $install, $p ) ) {
8186 if ( ( is_bool( $install->{$p} ) || ! empty( $install->{$p} ) ) &&
8187 $install->{$p} != $v
8188 ) {
8189 $install->{$p} = $v;
8190 $diff[ $p ] = $v;
8191 }
8192 } else {
8193 $special[ $p ] = $v;
8194
8195 if ( isset( $override[ $p ] ) ||
8196 'plugins' === $p ||
8197 'themes' === $p
8198 ) {
8199 $special_override = true;
8200 }
8201 }
8202 }
8203
8204 if ( $special_override || 0 < count( $diff ) ) {
8205 // Add special params only if has at least one
8206 // standard param, or if explicitly requested to
8207 // override a special param or a param which is not exist
8208 // in the install object.
8209 $diff = array_merge( $diff, $special );
8210 }
8211
8212 return $diff;
8213 }
8214
8215 /**
8216 * Update install only if changed.
8217 *
8218 * @author Vova Feldman (@svovaf)
8219 * @since 1.0.9
8220 *
8221 * @param string[] string $override
8222 * @param bool $flush
8223 *
8224 * @return false|object|string
8225 */
8226 private function send_install_update( $override = array(), $flush = false ) {
8227 $this->_logger->entrance();
8228
8229 $check_properties = $this->get_install_data_for_api( $override );
8230
8231 if ( $flush ) {
8232 $params = $check_properties;
8233 } else {
8234 $params = $this->get_install_diff_for_api( $check_properties, $this->_site, $override );
8235 }
8236
8237 $keepalive_only_update = false;
8238 if ( empty( $params ) ) {
8239 $keepalive_only_update = $this->should_send_keepalive_update();
8240
8241 if ( ! $keepalive_only_update ) {
8242 /**
8243 * There are no updates to send including keepalive.
8244 *
8245 * @author Leo Fajardo (@leorw)
8246 * @since 2.2.3
8247 */
8248 return false;
8249 }
8250 }
8251
8252 if ( ! $keepalive_only_update ) {
8253 /**
8254 * Do not update the last install sync timestamp after a keepalive-only call since there were no actual
8255 * updates sent.
8256 *
8257 * @author Leo Fajardo (@leorw)
8258 * @since 2.2.3
8259 */
8260 if ( ! is_multisite() ) {
8261 // Update last install sync timestamp.
8262 $this->set_cron_execution_timestamp( 'install_sync' );
8263 }
8264
8265 $params['uid'] = $this->get_anonymous_id();
8266 }
8267
8268 $this->set_keepalive_timestamp();
8269
8270 // Send updated values to FS.
8271 $site = $this->get_api_site_scope()->call( '/', 'put', $params );
8272
8273 if ( ! $keepalive_only_update && $this->is_api_result_entity( $site ) ) {
8274 /**
8275 * Do not clear scheduled sync after a keepalive-only call since there were no actual updates sent.
8276 *
8277 * @author Leo Fajardo (@leorw)
8278 * @since 2.2.3
8279 */
8280 if ( ! is_multisite() ) {
8281 // I successfully sent install update, clear scheduled sync if exist.
8282 $this->clear_install_sync_cron();
8283 }
8284 }
8285
8286 return $site;
8287 }
8288
8289 /**
8290 * Update installs only if changed.
8291 *
8292 * @author Vova Feldman (@svovaf)
8293 * @since 2.0.0
8294 *
8295 * @param string[] string $override
8296 * @param bool $flush
8297 *
8298 * @return false|object|string
8299 */
8300 private function send_installs_update( $override = array(), $flush = false ) {
8301 $this->_logger->entrance();
8302
8303 $installs_data = $this->get_installs_data_for_api( $override, ! $flush );
8304
8305 $keepalive_only_update = false;
8306 if ( empty( $installs_data ) ) {
8307 /**
8308 * Pass `true` to use the network level storage since the update is for many installs.
8309 *
8310 * @author Leo Fajardo (@leorw)
8311 * @since 2.2.3
8312 */
8313 $keepalive_only_update = $this->should_send_keepalive_update( true );
8314
8315 if ( ! $keepalive_only_update ) {
8316 /**
8317 * There are no updates to send including keepalive.
8318 *
8319 * @author Leo Fajardo (@leorw)
8320 * @since 2.2.3
8321 */
8322 return false;
8323 }
8324 }
8325
8326 if ( ! $keepalive_only_update ) {
8327 // Update last install sync timestamp if there were actual updates sent (i.e., not a keepalive-only call).
8328 $this->set_cron_execution_timestamp( 'install_sync' );
8329 }
8330
8331 /**
8332 * Pass `true` to use the network level storage since the update is for many installs.
8333 *
8334 * @author Leo Fajardo (@leorw)
8335 * @since 2.2.3
8336 */
8337 $this->set_keepalive_timestamp( true );
8338
8339 // Send updated values to FS.
8340 $result = $this->get_api_user_scope()->call( "/plugins/{$this->_plugin->id}/installs.json", 'put', $installs_data );
8341
8342 if ( ! $keepalive_only_update && $this->is_api_result_object( $result, 'installs' ) ) {
8343 // 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.
8344 $this->clear_install_sync_cron();
8345 }
8346
8347 return $result;
8348 }
8349
8350 /**
8351 * @author Leo Fajardo (@leorw)
8352 *
8353 * @param bool|null $use_network_level_storage
8354 *
8355 * @return bool
8356 */
8357 private function should_send_keepalive_update( $use_network_level_storage = null ) {
8358 $keepalive_timestamp = $this->_storage->get( 'keepalive_timestamp', 0, $use_network_level_storage );
8359
8360 if ( $keepalive_timestamp < ( time() - WP_FS__TIME_WEEK_IN_SEC ) ) {
8361 // If updated more than 7 days ago, trigger a keepalive and update the time it was triggered.
8362 return true;
8363 } else {
8364 // 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.
8365 return ( 7 == rand( 1, 7 ) );
8366 }
8367 }
8368
8369 /**
8370 * Update install only if changed.
8371 *
8372 * @author Vova Feldman (@svovaf)
8373 * @since 1.0.9
8374 *
8375 * @param string[] string $override
8376 * @param bool $flush
8377 */
8378 private function sync_install( $override = array(), $flush = false ) {
8379 $this->_logger->entrance();
8380
8381 $site = $this->send_install_update( $override, $flush );
8382
8383 if ( false === $site ) {
8384 // No sync required.
8385 return;
8386 }
8387
8388 if ( ! $this->is_api_result_entity( $site ) ) {
8389 // Failed to sync, don't update locally.
8390 return;
8391 }
8392
8393 $this->_site = new FS_Site( $site );
8394
8395 $this->_store_site( true );
8396 }
8397
8398 /**
8399 * Update install only if changed.
8400 *
8401 * @author Vova Feldman (@svovaf)
8402 * @since 1.0.9
8403 *
8404 * @param string[] string $override
8405 * @param bool $flush
8406 */
8407 private function sync_installs( $override = array(), $flush = false ) {
8408 $this->_logger->entrance();
8409
8410 $result = $this->send_installs_update( $override, $flush );
8411
8412 if ( false === $result ) {
8413 // No sync required.
8414 return;
8415 }
8416
8417 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
8418 // Failed to sync, don't update locally.
8419 return;
8420 }
8421
8422 $address_to_blog_map = $this->get_address_to_blog_map();
8423
8424 foreach ( $result->installs as $install ) {
8425 $this->_site = new FS_Site( $install );
8426
8427 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
8428 $blog_id = $address_to_blog_map[ $address ];
8429
8430 $this->_store_site( true, $blog_id );
8431 }
8432 }
8433
8434 /**
8435 * Track install's custom event.
8436 *
8437 * IMPORTANT:
8438 * Custom event tracking is currently only supported for specific clients.
8439 * If you are not one of them, please don't use this method. If you will,
8440 * the API will simply ignore your request based on the plugin ID.
8441 *
8442 * Need custom tracking for your plugin or theme?
8443 * If you are interested in custom event tracking please contact yo@freemius.com
8444 * for further details.
8445 *
8446 * @author Vova Feldman (@svovaf)
8447 * @since 1.2.1
8448 *
8449 * @param string $name Event name.
8450 * @param array $properties Associative key/value array with primitive values only
8451 * @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
8452 * @param bool $once If true, event will be tracked only once. IMPORTANT: Still trigger the API call.
8453 *
8454 * @return object|false Event data or FALSE on failure.
8455 *
8456 * @throws \Freemius_InvalidArgumentException
8457 */
8458 public function track_event( $name, $properties = array(), $process_at = false, $once = false ) {
8459 $this->_logger->entrance( http_build_query( array( 'name' => $name, 'once' => $once ) ) );
8460
8461 if ( ! $this->is_registered() ) {
8462 return false;
8463 }
8464
8465 $event = array( 'type' => $name );
8466
8467 if ( is_numeric( $process_at ) && $process_at > time() ) {
8468 $event['process_at'] = $process_at;
8469 }
8470
8471 if ( $once ) {
8472 $event['once'] = true;
8473 }
8474
8475 if ( ! empty( $properties ) ) {
8476 // Verify associative array values are primitive.
8477 foreach ( $properties as $k => $v ) {
8478 if ( ! is_scalar( $v ) ) {
8479 throw new Freemius_InvalidArgumentException( 'The $properties argument must be an associative key/value array with primitive values only.' );
8480 }
8481 }
8482
8483 $event['properties'] = $properties;
8484 }
8485
8486 $result = $this->get_api_site_scope()->call( 'events.json', 'post', $event );
8487
8488 return $this->is_api_error( $result ) ?
8489 false :
8490 $result;
8491 }
8492
8493 /**
8494 * Track install's custom event only once, but it still triggers the API call.
8495 *
8496 * IMPORTANT:
8497 * Custom event tracking is currently only supported for specific clients.
8498 * If you are not one of them, please don't use this method. If you will,
8499 * the API will simply ignore your request based on the plugin ID.
8500 *
8501 * Need custom tracking for your plugin or theme?
8502 * If you are interested in custom event tracking please contact yo@freemius.com
8503 * for further details.
8504 *
8505 * @author Vova Feldman (@svovaf)
8506 * @since 1.2.1
8507 *
8508 * @param string $name Event name.
8509 * @param array $properties Associative key/value array with primitive values only
8510 * @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
8511 *
8512 * @return object|false Event data or FALSE on failure.
8513 *
8514 * @throws \Freemius_InvalidArgumentException
8515 *
8516 * @user Freemius::track_event()
8517 */
8518 public function track_event_once( $name, $properties = array(), $process_at = false ) {
8519 return $this->track_event( $name, $properties, $process_at, true );
8520 }
8521
8522 /**
8523 * Plugin uninstall hook.
8524 *
8525 * @author Vova Feldman (@svovaf)
8526 * @since 1.0.1
8527 *
8528 * @param bool $check_user Enforce checking if user have plugins activation privileges.
8529 */
8530 function _uninstall_plugin_event( $check_user = true ) {
8531 $this->_logger->entrance( 'slug = ' . $this->_slug );
8532
8533 if ( $check_user && ! current_user_can( 'activate_plugins' ) ) {
8534 return;
8535 }
8536
8537 $params = array();
8538 $uninstall_reason = null;
8539 if ( isset( $this->_storage->uninstall_reason ) ) {
8540 $uninstall_reason = $this->_storage->uninstall_reason;
8541 $params['reason_id'] = $uninstall_reason->id;
8542 $params['reason_info'] = $uninstall_reason->info;
8543 }
8544
8545 if ( ! $this->is_registered() ) {
8546 // Send anonymous uninstall event only if user submitted a feedback.
8547 if ( isset( $uninstall_reason ) ) {
8548 if ( isset( $uninstall_reason->is_anonymous ) && ! $uninstall_reason->is_anonymous ) {
8549 $this->opt_in( false, false, false, false, true );
8550 } else {
8551 $params['uid'] = $this->get_anonymous_id();
8552 $this->get_api_plugin_scope()->call( 'uninstall.json', 'put', $params );
8553 }
8554 }
8555 } else {
8556 $params = array_merge( $params, array(
8557 'is_active' => false,
8558 'is_uninstalled' => true,
8559 ) );
8560
8561 if ( $this->_is_network_active ) {
8562 // Send uninstall event.
8563 $this->send_installs_update( $params );
8564 } else {
8565 // Send uninstall event.
8566 $this->send_install_update( $params );
8567 }
8568 }
8569
8570 // @todo Decide if we want to delete plugin information from db.
8571 }
8572
8573 /**
8574 * Set the basename of the current product and hook _activate_plugin_event_hook() to the activation action.
8575 *
8576 * @author Vova Feldman (@svovaf)
8577 * @since 2.2.1
8578 *
8579 * @param string $is_premium
8580 * @param string $caller
8581 *
8582 * @return string
8583 */
8584 function set_basename( $is_premium, $caller ) {
8585 $basename = plugin_basename( $caller );
8586
8587 $current_basename = $is_premium ?
8588 $this->_premium_plugin_basename :
8589 $this->_free_plugin_basename;
8590
8591 if ( $current_basename == $basename ) {
8592 // Basename value set correctly.
8593 return;
8594 }
8595
8596 if ( $is_premium ) {
8597 $this->_premium_plugin_basename = $basename;
8598 } else {
8599 $this->_free_plugin_basename = $basename;
8600 }
8601
8602 $plugin_dir = dirname( $this->_plugin_dir_path ) . '/';
8603
8604 register_activation_hook(
8605 $plugin_dir . $basename,
8606 array( &$this, '_activate_plugin_event_hook' )
8607 );
8608 }
8609
8610 /**
8611 * @author Vova Feldman (@svovaf)
8612 * @since 1.1.1
8613 * @since 2.2.1 If the context product is in its premium version, use the current module's basename, even if it was renamed.
8614 *
8615 * @return string
8616 */
8617 function premium_plugin_basename() {
8618 if ( ! isset( $this->_premium_plugin_basename ) ) {
8619 $this->_premium_plugin_basename = $this->is_premium() ?
8620 // The product is premium, so use the current basename.
8621 $this->_plugin_basename :
8622 $this->get_premium_slug() . '/' . basename( $this->_free_plugin_basename );
8623 }
8624
8625 return $this->_premium_plugin_basename;
8626 }
8627
8628 /**
8629 * Uninstall plugin hook. Called only when connected his account with Freemius for active sites tracking.
8630 *
8631 * @author Vova Feldman (@svovaf)
8632 * @since 1.0.2
8633 */
8634 public static function _uninstall_plugin_hook() {
8635 self::_load_required_static();
8636
8637 self::$_static_logger->entrance();
8638
8639 if ( ! current_user_can( 'activate_plugins' ) ) {
8640 return;
8641 }
8642
8643 $plugin_file = substr( current_filter(), strlen( 'uninstall_' ) );
8644
8645 self::$_static_logger->info( 'plugin = ' . $plugin_file );
8646
8647 define( 'WP_FS__UNINSTALL_MODE', true );
8648
8649 $fs = self::get_instance_by_file( $plugin_file );
8650
8651 if ( is_object( $fs ) ) {
8652 self::require_plugin_essentials();
8653
8654 if ( is_plugin_active( $fs->_free_plugin_basename ) ||
8655 is_plugin_active( $fs->premium_plugin_basename() )
8656 ) {
8657 // Deleting Free or Premium plugin version while the other version still installed.
8658 return;
8659 }
8660
8661 $fs->_uninstall_plugin_event();
8662
8663 $fs->do_action( 'after_uninstall' );
8664 }
8665 }
8666
8667 #----------------------------------------------------------------------------------
8668 #region Plugin Information
8669 #----------------------------------------------------------------------------------
8670
8671 /**
8672 * Load WordPress core plugin.php essential module.
8673 *
8674 * @author Vova Feldman (@svovaf)
8675 * @since 1.1.1
8676 */
8677 private static function require_plugin_essentials() {
8678 if ( ! function_exists( 'get_plugins' ) ) {
8679 self::$_static_logger->log( 'Including wp-admin/includes/plugin.php...' );
8680
8681 require_once ABSPATH . 'wp-admin/includes/plugin.php';
8682 }
8683 }
8684
8685 /**
8686 * Load WordPress core pluggable.php module.
8687 *
8688 * @author Vova Feldman (@svovaf)
8689 * @since 1.1.2
8690 */
8691 private static function require_pluggable_essentials() {
8692 if ( ! function_exists( 'wp_get_current_user' ) ) {
8693 require_once ABSPATH . 'wp-includes/pluggable.php';
8694 }
8695 }
8696
8697 /**
8698 * Return plugin data.
8699 *
8700 * @author Vova Feldman (@svovaf)
8701 * @since 1.0.1
8702 *
8703 * @return array
8704 */
8705 function get_plugin_data() {
8706 if ( ! isset( $this->_plugin_data ) ) {
8707 self::require_plugin_essentials();
8708
8709 if ( $this->is_plugin() ) {
8710 /**
8711 * @author Vova Feldman (@svovaf)
8712 * @since 1.2.0 When using get_plugin_data() do NOT translate plugin data.
8713 *
8714 * @link https://github.com/Freemius/wordpress-sdk/issues/77
8715 */
8716 $plugin_data = get_plugin_data(
8717 $this->_plugin_main_file_path,
8718 false,
8719 false
8720 );
8721 } else {
8722 $theme_data = wp_get_theme();
8723
8724 if ( $this->_plugin_basename !== $theme_data->get_stylesheet() && is_child_theme() ) {
8725 $parent_theme = $theme_data->parent();
8726
8727 if ( ( $parent_theme instanceof WP_Theme ) && $this->_plugin_basename === $parent_theme->get_stylesheet() ) {
8728 $theme_data = $parent_theme;
8729 }
8730 }
8731
8732 $plugin_data = array(
8733 'Name' => $theme_data->get( 'Name' ),
8734 'Version' => $theme_data->get( 'Version' ),
8735 'Author' => $theme_data->get( 'Author' ),
8736 'Description' => $theme_data->get( 'Description' ),
8737 'PluginURI' => $theme_data->get( 'ThemeURI' ),
8738 );
8739 }
8740
8741 $this->_plugin_data = $plugin_data;
8742 }
8743
8744 return $this->_plugin_data;
8745 }
8746
8747 /**
8748 * @author Vova Feldman (@svovaf)
8749 * @since 1.0.1
8750 * @since 1.2.2.5 If slug not set load slug by module ID.
8751 *
8752 * @return string Plugin slug.
8753 */
8754 function get_slug() {
8755 if ( ! isset( $this->_slug ) ) {
8756 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
8757 $this->_slug = $id_slug_type_path_map[ $this->_module_id ]['slug'];
8758 }
8759
8760 return $this->_slug;
8761 }
8762
8763 /**
8764 * @author Leo Fajardo (@leorw)
8765 * @since 2.2.1
8766 *
8767 * @return string
8768 */
8769 function get_premium_slug() {
8770 return is_object( $this->_plugin ) ?
8771 $this->_plugin->premium_slug :
8772 "{$this->_slug}-premium";
8773 }
8774
8775 /**
8776 * Retrieve the desired folder name for the product.
8777 *
8778 * @author Vova Feldman (@svovaf)
8779 * @since 1.2.1.7
8780 *
8781 * @return string Plugin slug.
8782 */
8783 function get_target_folder_name() {
8784 return $this->can_use_premium_code() ?
8785 $this->_plugin->premium_slug :
8786 $this->_slug;
8787 }
8788
8789 /**
8790 * @author Vova Feldman (@svovaf)
8791 * @since 1.0.1
8792 *
8793 * @return number Plugin ID.
8794 */
8795 function get_id() {
8796 return $this->_plugin->id;
8797 }
8798
8799 /**
8800 * @author Vova Feldman (@svovaf)
8801 * @since 1.2.1.5
8802 *
8803 * @return string Freemius SDK version
8804 */
8805 function get_sdk_version() {
8806 return $this->version;
8807 }
8808
8809 /**
8810 * @author Vova Feldman (@svovaf)
8811 * @since 1.2.1.5
8812 *
8813 * @return number Parent plugin ID (if parent exist).
8814 */
8815 function get_parent_id() {
8816 return $this->is_addon() ?
8817 $this->get_parent_instance()->get_id() :
8818 $this->_plugin->id;
8819 }
8820
8821 /**
8822 * @author Vova Feldman (@svovaf)
8823 * @since 1.0.1
8824 *
8825 * @return string Plugin public key.
8826 */
8827 function get_public_key() {
8828 return $this->_plugin->public_key;
8829 }
8830
8831 /**
8832 * Will be available only on sandbox mode.
8833 *
8834 * @author Vova Feldman (@svovaf)
8835 * @since 1.0.4
8836 *
8837 * @return mixed Plugin secret key.
8838 */
8839 function get_secret_key() {
8840 return $this->_plugin->secret_key;
8841 }
8842
8843 /**
8844 * @author Vova Feldman (@svovaf)
8845 * @since 1.1.1
8846 *
8847 * @return bool
8848 */
8849 function has_secret_key() {
8850 return ! empty( $this->_plugin->secret_key );
8851 }
8852
8853 /**
8854 * @author Vova Feldman (@svovaf)
8855 * @since 1.0.9
8856 *
8857 * @param string|bool $premium_suffix
8858 *
8859 * @return string
8860 */
8861 function get_plugin_name( $premium_suffix = false ) {
8862 $this->_logger->entrance();
8863
8864 /**
8865 * This `if-else` can be squeezed into a single `if` but I intentionally split it for code readability.
8866 *
8867 * @author Vova Feldman
8868 */
8869 if ( ! isset( $this->_plugin_name ) ) {
8870 // Name is not yet set.
8871 $this->set_name( $premium_suffix );
8872 } else if (
8873 ! empty( $premium_suffix ) &&
8874 ( ! is_object( $this->_plugin ) || $this->_plugin->premium_suffix !== $premium_suffix )
8875 ) {
8876 // Name is already set, but there's a change in the premium suffix.
8877 $this->set_name( $premium_suffix );
8878 }
8879
8880 return $this->_plugin_name;
8881 }
8882
8883 /**
8884 * Calculates and stores the product's name. This helper function was created specifically for get_plugin_name() just to make the code clearer.
8885 *
8886 * @author Vova Feldman (@svovaf)
8887 * @since 2.2.1
8888 *
8889 * @param string $premium_suffix
8890 */
8891 private function set_name( $premium_suffix = '' ) {
8892 $plugin_data = $this->get_plugin_data();
8893
8894 // Get name.
8895 $this->_plugin_name = $plugin_data['Name'];
8896
8897 if ( is_string( $premium_suffix ) ) {
8898 $premium_suffix = trim( $premium_suffix );
8899
8900 if ( ! empty( $premium_suffix ) ) {
8901 // Check if plugin name contains " (premium)" or a custom suffix and remove it.
8902 $suffix = ( ' ' . strtolower( $premium_suffix ) );
8903 $suffix_len = strlen( $suffix );
8904
8905 if ( strlen( $plugin_data['Name'] ) > $suffix_len &&
8906 $suffix === substr( strtolower( $plugin_data['Name'] ), - $suffix_len )
8907 ) {
8908 $this->_plugin_name = substr( $plugin_data['Name'], 0, - $suffix_len );
8909 }
8910 }
8911 }
8912
8913 $this->_logger->departure( 'Name = ' . $this->_plugin_name );
8914 }
8915
8916 /**
8917 * @author Vova Feldman (@svovaf)
8918 * @since 1.0.0
8919 *
8920 * @return string
8921 */
8922 function get_plugin_version() {
8923 $this->_logger->entrance();
8924
8925 $plugin_data = $this->get_plugin_data();
8926
8927 $this->_logger->departure( 'Version = ' . $plugin_data['Version'] );
8928
8929 return $this->apply_filters( 'plugin_version', $plugin_data['Version'] );
8930 }
8931
8932 /**
8933 * @author Vova Feldman (@svovaf)
8934 * @since 1.2.1.7
8935 *
8936 * @return string
8937 */
8938 function get_plugin_title() {
8939 $this->_logger->entrance();
8940
8941 $title = $this->_plugin->title;
8942
8943 return $this->apply_filters( 'plugin_title', $title );
8944 }
8945
8946 /**
8947 * @author Vova Feldman (@svovaf)
8948 * @since 1.2.2.7
8949 *
8950 * @param bool $lowercase
8951 *
8952 * @return string
8953 */
8954 function get_module_label( $lowercase = false ) {
8955 $label = $this->is_addon() ?
8956 $this->get_text_inline( 'Add-On', 'addon' ) :
8957 ( $this->is_plugin() ?
8958 $this->get_text_inline( 'Plugin', 'plugin' ) :
8959 $this->get_text_inline( 'Theme', 'theme' ) );
8960
8961 if ( $lowercase ) {
8962 $label = strtolower( $label );
8963 }
8964
8965 return $label;
8966 }
8967
8968 /**
8969 * @author Vova Feldman (@svovaf)
8970 * @since 1.0.4
8971 *
8972 * @return string
8973 */
8974 function get_plugin_basename() {
8975 if ( ! isset( $this->_plugin_basename ) ) {
8976 if ( $this->is_plugin() ) {
8977 $this->_plugin_basename = plugin_basename( $this->_plugin_main_file_path );
8978 } else {
8979 $this->_plugin_basename = basename( dirname( $this->_plugin_main_file_path ) );
8980 }
8981 }
8982
8983 return $this->_plugin_basename;
8984 }
8985
8986 function get_plugin_folder_name() {
8987 $this->_logger->entrance();
8988
8989 $plugin_folder = $this->_plugin_basename;
8990
8991 while ( '.' !== dirname( $plugin_folder ) ) {
8992 $plugin_folder = dirname( $plugin_folder );
8993 }
8994
8995 $this->_logger->departure( 'Folder Name = ' . $plugin_folder );
8996
8997 return $plugin_folder;
8998 }
8999
9000 #endregion ------------------------------------------------------------------
9001
9002 /* Account
9003 ------------------------------------------------------------------------------------------------------------------*/
9004
9005 /**
9006 * Find plugin's slug by plugin's basename.
9007 *
9008 * @author Vova Feldman (@svovaf)
9009 * @since 1.0.9
9010 *
9011 * @param string $plugin_base_name
9012 *
9013 * @return false|string
9014 */
9015 private static function find_slug_by_basename( $plugin_base_name ) {
9016 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
9017
9018 if ( ! array( $file_slug_map ) || ! isset( $file_slug_map[ $plugin_base_name ] ) ) {
9019 return false;
9020 }
9021
9022 return $file_slug_map[ $plugin_base_name ];
9023 }
9024
9025 /**
9026 * Store the map between the plugin's basename to the slug.
9027 *
9028 * @author Vova Feldman (@svovaf)
9029 * @since 1.0.9
9030 */
9031 private function store_file_slug_map() {
9032 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
9033
9034 if ( ! array( $file_slug_map ) ) {
9035 $file_slug_map = array();
9036 }
9037
9038 if ( ! isset( $file_slug_map[ $this->_plugin_basename ] ) ||
9039 $file_slug_map[ $this->_plugin_basename ] !== $this->_slug
9040 ) {
9041 $file_slug_map[ $this->_plugin_basename ] = $this->_slug;
9042 self::$_accounts->set_option( 'file_slug_map', $file_slug_map, true );
9043 }
9044 }
9045
9046 /**
9047 * @return array[number]FS_User
9048 */
9049 static function get_all_users() {
9050 $users = self::$_accounts->get_option( 'users', array() );
9051
9052 if ( ! is_array( $users ) ) {
9053 $users = array();
9054 }
9055
9056 return $users;
9057 }
9058
9059 /**
9060 * @param string $module_type
9061 * @param null|int $blog_id Since 2.0.0
9062 *
9063 * @return array[string]FS_Site
9064 */
9065 private static function get_all_sites(
9066 $module_type = WP_FS__MODULE_TYPE_PLUGIN,
9067 $blog_id = null
9068 ) {
9069 $sites = self::get_account_option( 'sites', $module_type, $blog_id );
9070
9071 if ( ! is_array( $sites ) ) {
9072 $sites = array();
9073 }
9074
9075 return $sites;
9076 }
9077
9078 /**
9079 * @author Leo Fajardo (@leorw)
9080 *
9081 * @since 1.2.2
9082 *
9083 * @param string $option_name
9084 * @param string $module_type
9085 * @param null|int $network_level_or_blog_id Since 2.0.0
9086 *
9087 * @return mixed
9088 */
9089 private static function get_account_option( $option_name, $module_type = null, $network_level_or_blog_id = null ) {
9090 if ( ! is_null( $module_type ) && WP_FS__MODULE_TYPE_PLUGIN !== $module_type ) {
9091 $option_name = $module_type . '_' . $option_name;
9092 }
9093
9094 return self::$_accounts->get_option( $option_name, array(), $network_level_or_blog_id );
9095 }
9096
9097 /**
9098 * @author Leo Fajardo (@leorw)
9099 *
9100 * @since 1.2.2
9101 *
9102 * @param string $option_name
9103 * @param mixed $option_value
9104 * @param bool $store
9105 * @param null|int $network_level_or_blog_id Since 2.0.0
9106 */
9107 private function set_account_option( $option_name, $option_value, $store, $network_level_or_blog_id = null ) {
9108 self::set_account_option_by_module(
9109 $this->_module_type,
9110 $option_name,
9111 $option_value,
9112 $store,
9113 $network_level_or_blog_id
9114 );
9115 }
9116
9117 /**
9118 * @author Vova Feldman (@svovaf)
9119 *
9120 * @since 1.2.2.7
9121 *
9122 * @param string $module_type
9123 * @param string $option_name
9124 * @param mixed $option_value
9125 * @param bool $store
9126 * @param null|int $network_level_or_blog_id Since 2.0.0
9127 */
9128 private static function set_account_option_by_module(
9129 $module_type,
9130 $option_name,
9131 $option_value,
9132 $store,
9133 $network_level_or_blog_id = null
9134 ) {
9135 if ( WP_FS__MODULE_TYPE_PLUGIN != $module_type ) {
9136 $option_name = $module_type . '_' . $option_name;
9137 }
9138
9139 self::$_accounts->set_option( $option_name, $option_value, $store, $network_level_or_blog_id );
9140 }
9141
9142 /**
9143 * @author Vova Feldman (@svovaf)
9144 * @since 1.0.6
9145 *
9146 * @param number|null $module_id
9147 *
9148 * @return FS_Plugin_License[]
9149 */
9150 private static function get_all_licenses( $module_id = null ) {
9151 $licenses = self::get_account_option( 'all_licenses' );
9152
9153 if ( ! is_array( $licenses ) ) {
9154 $licenses = array();
9155 }
9156
9157 if ( is_null( $module_id ) ) {
9158 return $licenses;
9159 }
9160
9161 $licenses = isset( $licenses[ $module_id ] ) ?
9162 $licenses[ $module_id ] :
9163 array();
9164
9165 return $licenses;
9166 }
9167
9168 /**
9169 * @author Leo Fajardo (@leorw)
9170 * @since 2.0.0
9171 *
9172 * @return array
9173 */
9174 private static function get_all_licenses_by_module_type() {
9175 $licenses = self::get_account_option( 'all_licenses' );
9176
9177 $licenses_by_module_type = array(
9178 WP_FS__MODULE_TYPE_PLUGIN => array(),
9179 WP_FS__MODULE_TYPE_THEME => array()
9180 );
9181
9182 if ( ! is_array( $licenses ) ) {
9183 return $licenses_by_module_type;
9184 }
9185
9186 foreach ( $licenses as $module_id => $module_licenses ) {
9187 $fs = self::get_instance_by_id( $module_id );
9188 if ( false === $fs ) {
9189 continue;
9190 }
9191
9192 $licenses_by_module_type[ $fs->_module_type ] = array_merge( $licenses_by_module_type[ $fs->_module_type ], $module_licenses );
9193 }
9194
9195 return $licenses_by_module_type;
9196 }
9197
9198 /**
9199 * @author Leo Fajardo (@leorw)
9200 * @since 2.0.0
9201 *
9202 * @param number $module_id
9203 * @param number|null $user_id
9204 *
9205 * @return array
9206 */
9207 private static function get_user_id_license_ids_map( $module_id, $user_id = null ) {
9208 $all_modules_user_id_license_ids_map = self::get_account_option( 'user_id_license_ids_map' );
9209
9210 if ( ! is_array( $all_modules_user_id_license_ids_map ) ) {
9211 $all_modules_user_id_license_ids_map = array();
9212 }
9213
9214 $user_id_license_ids_map = isset( $all_modules_user_id_license_ids_map[ $module_id ] ) ?
9215 $all_modules_user_id_license_ids_map[ $module_id ] :
9216 array();
9217
9218 if ( FS_User::is_valid_id( $user_id ) ) {
9219 $user_id_license_ids_map = isset( $user_id_license_ids_map[ $user_id ] ) ?
9220 $user_id_license_ids_map[ $user_id ] :
9221 array();
9222 }
9223
9224 return $user_id_license_ids_map;
9225 }
9226
9227 /**
9228 * @author Leo Fajardo (@leorw)
9229 * @since 2.0.0
9230 *
9231 * @param array $new_user_id_license_ids_map
9232 * @param number $module_id
9233 * @param number|null $user_id
9234 */
9235 private static function store_user_id_license_ids_map( $new_user_id_license_ids_map, $module_id, $user_id = null ) {
9236 $all_modules_user_id_license_ids_map = self::get_account_option( 'user_id_license_ids_map' );
9237 if ( ! is_array( $all_modules_user_id_license_ids_map ) ) {
9238 $all_modules_user_id_license_ids_map = array();
9239 }
9240
9241 if ( ! isset( $all_modules_user_id_license_ids_map[ $module_id ] ) ) {
9242 $all_modules_user_id_license_ids_map[ $module_id ] = array();
9243 }
9244
9245 if ( FS_User::is_valid_id( $user_id ) ) {
9246 $all_modules_user_id_license_ids_map[ $module_id ][ $user_id ] = $new_user_id_license_ids_map;
9247 } else {
9248 $all_modules_user_id_license_ids_map[ $module_id ] = $new_user_id_license_ids_map;
9249 }
9250
9251 self::$_accounts->set_option( 'user_id_license_ids_map', $all_modules_user_id_license_ids_map, true );
9252 }
9253
9254 /**
9255 * Get a collection of the user's linked license IDs.
9256 *
9257 * @author Vova Feldman (@svovaf)
9258 * @since 2.0.0
9259 *
9260 * @param number $user_id
9261 *
9262 * @return number[]
9263 */
9264 private function get_user_linked_license_ids( $user_id ) {
9265 return self::get_user_id_license_ids_map( $this->_module_id, $user_id );
9266 }
9267
9268 /**
9269 * Override the user's linked license IDs with a new IDs collection.
9270 *
9271 * @author Vova Feldman (@svovaf)
9272 * @since 2.0.0
9273 *
9274 * @param number $user_id
9275 * @param number[] $license_ids
9276 */
9277 private function set_user_linked_license_ids( $user_id, array $license_ids ) {
9278 self::store_user_id_license_ids_map( $license_ids, $this->_module_id, $user_id );
9279 }
9280
9281 /**
9282 * Link a specified license ID to a given user.
9283 *
9284 * @author Vova Feldman (@svovaf)
9285 * @since 2.0.0
9286 *
9287 * @param number $license_id
9288 * @param number $user_id
9289 */
9290 private function link_license_2_user( $license_id, $user_id ) {
9291 $license_ids = $this->get_user_linked_license_ids( $user_id );
9292
9293 if ( in_array( $license_id, $license_ids ) ) {
9294 // License already linked.
9295 return;
9296 }
9297
9298 $license_ids[] = $license_id;
9299
9300 $this->set_user_linked_license_ids( $user_id, $license_ids );
9301 }
9302
9303 /**
9304 * @param string|bool $module_type
9305 *
9306 * @return FS_Plugin_Plan[]
9307 */
9308 private static function get_all_plans( $module_type = false ) {
9309 $plans = self::get_account_option( 'plans', $module_type );
9310
9311 if ( ! is_array( $plans ) ) {
9312 $plans = array();
9313 }
9314
9315 return $plans;
9316 }
9317
9318 /**
9319 * @author Vova Feldman (@svovaf)
9320 * @since 1.0.4
9321 *
9322 * @return FS_Plugin_Tag[]
9323 */
9324 private static function get_all_updates() {
9325 $updates = self::$_accounts->get_option( 'updates', array() );
9326
9327 if ( ! is_array( $updates ) ) {
9328 $updates = array();
9329 }
9330
9331 return $updates;
9332 }
9333
9334 /**
9335 * @author Vova Feldman (@svovaf)
9336 * @since 1.0.6
9337 *
9338 * @return array<number,FS_Plugin[]>|false
9339 */
9340 private static function get_all_addons() {
9341 $addons = self::$_accounts->get_option( 'addons', array() );
9342
9343 if ( ! is_array( $addons ) ) {
9344 $addons = array();
9345 }
9346
9347 return $addons;
9348 }
9349
9350 /**
9351 * @author Vova Feldman (@svovaf)
9352 * @since 1.0.6
9353 *
9354 * @return FS_Plugin[]|false
9355 */
9356 private static function get_all_account_addons() {
9357 $addons = self::$_accounts->get_option( 'account_addons', array() );
9358
9359 if ( ! is_array( $addons ) ) {
9360 $addons = array();
9361 }
9362
9363 return $addons;
9364 }
9365
9366 /**
9367 * Check if user has connected his account (opted-in).
9368 *
9369 * Note:
9370 * If the user opted-in and opted-out on a later stage,
9371 * this will still return true. If you want to check if the
9372 * user is currently opted-in, use:
9373 * `$fs->is_registered() && $fs->is_tracking_allowed()`
9374 *
9375 * @author Vova Feldman (@svovaf)
9376 * @since 1.0.1
9377 * @return bool
9378 */
9379 function is_registered() {
9380 return is_object( $this->_user );
9381 }
9382
9383 /**
9384 * Returns TRUE if the user opted-in and didn't disconnect (opt-out).
9385 *
9386 * @author Leo Fajardo (@leorw)
9387 * @since 1.2.1.5
9388 *
9389 * @return bool
9390 */
9391 function is_tracking_allowed() {
9392 return ( is_object( $this->_site ) && $this->_site->is_tracking_allowed() );
9393 }
9394
9395 /**
9396 * @author Vova Feldman (@svovaf)
9397 * @since 1.0.4
9398 *
9399 * @return FS_Plugin
9400 */
9401 function get_plugin() {
9402 return $this->_plugin;
9403 }
9404
9405 /**
9406 * @author Vova Feldman (@svovaf)
9407 * @since 1.0.3
9408 *
9409 * @return FS_User
9410 */
9411 function get_user() {
9412 return $this->_user;
9413 }
9414
9415 /**
9416 * @author Vova Feldman (@svovaf)
9417 * @since 1.0.3
9418 *
9419 * @return FS_Site
9420 */
9421 function get_site() {
9422 return $this->_site;
9423 }
9424
9425 /**
9426 * Get plugin add-ons.
9427 *
9428 * @author Vova Feldman (@svovaf)
9429 * @since 1.0.6
9430 *
9431 * @since 1.1.7.3 If not yet loaded, fetch data from the API.
9432 *
9433 * @param bool $flush
9434 *
9435 * @return FS_Plugin[]|false
9436 */
9437 function get_addons( $flush = false ) {
9438 $this->_logger->entrance();
9439
9440 if ( ! $this->_has_addons ) {
9441 return false;
9442 }
9443
9444 $addons = $this->sync_addons( $flush );
9445
9446 return ( ! is_array( $addons ) || empty( $addons ) ) ?
9447 false :
9448 $addons;
9449 }
9450
9451 /**
9452 * @author Vova Feldman (@svovaf)
9453 * @since 1.0.6
9454 *
9455 * @return FS_Plugin[]|false
9456 */
9457 function get_account_addons() {
9458 $this->_logger->entrance();
9459
9460 $addons = self::get_all_account_addons();
9461
9462 if ( ! is_array( $addons ) ||
9463 ! isset( $addons[ $this->_plugin->id ] ) ||
9464 ! is_array( $addons[ $this->_plugin->id ] ) ||
9465 0 === count( $addons[ $this->_plugin->id ] )
9466 ) {
9467 return false;
9468 }
9469
9470 return $addons[ $this->_plugin->id ];
9471 }
9472
9473 /**
9474 * Check if user has any
9475 *
9476 * @author Vova Feldman (@svovaf)
9477 * @since 1.1.6
9478 *
9479 * @return bool
9480 */
9481 function has_account_addons() {
9482 $addons = $this->get_account_addons();
9483
9484 return is_array( $addons ) && ( 0 < count( $addons ) );
9485 }
9486
9487
9488 /**
9489 * Get add-on by ID (from local data).
9490 *
9491 * @author Vova Feldman (@svovaf)
9492 * @since 1.0.6
9493 *
9494 * @param number $id
9495 *
9496 * @return FS_Plugin|false
9497 */
9498 function get_addon( $id ) {
9499 $this->_logger->entrance();
9500
9501 $addons = $this->get_addons();
9502
9503 if ( is_array( $addons ) ) {
9504 foreach ( $addons as $addon ) {
9505 if ( $id == $addon->id ) {
9506 return $addon;
9507 }
9508 }
9509 }
9510
9511 return false;
9512 }
9513
9514 /**
9515 * Get add-on by slug (from local data).
9516 *
9517 * @author Vova Feldman (@svovaf)
9518 * @since 1.0.6
9519 *
9520 * @param string $slug
9521 *
9522 * @param bool $flush
9523 *
9524 * @return FS_Plugin|false
9525 */
9526 function get_addon_by_slug( $slug, $flush = false ) {
9527 $this->_logger->entrance();
9528
9529 $addons = $this->get_addons( $flush );
9530
9531 if ( is_array( $addons ) ) {
9532 foreach ( $addons as $addon ) {
9533 if ( $slug === $addon->slug ) {
9534 return $addon;
9535 }
9536 }
9537 }
9538
9539 return false;
9540 }
9541
9542 /**
9543 * @author Vova Feldman (@svovaf)
9544 * @since 2.0.0
9545 *
9546 * @param number $user_id
9547 *
9548 * @return FS_User
9549 */
9550 static function _get_user_by_id( $user_id ) {
9551 self::$_static_logger->entrance( "user_id = {$user_id}" );
9552
9553 $users = self::get_all_users();
9554
9555 if ( is_array( $users ) ) {
9556 if ( isset( $users[ $user_id ] ) &&
9557 $users[ $user_id ] instanceof FS_User &&
9558 $user_id == $users[ $user_id ]->id
9559 ) {
9560 return $users[ $user_id ];
9561 }
9562
9563 // If user wasn't found by the key, iterate over all the users collection.
9564 foreach ( $users as $user ) {
9565 /**
9566 * @var FS_User $user
9567 */
9568 if ( $user_id == $user->id ) {
9569 return $user;
9570 }
9571 }
9572 }
9573
9574 return null;
9575 }
9576
9577 /**
9578 * Checks if a Freemius user_id is associated with a super-admin.
9579 *
9580 * @author Vova Feldman (@svovaf)
9581 * @since 2.0.0
9582 *
9583 * @param number $user_id
9584 *
9585 * @return bool
9586 */
9587 private static function is_super_admin( $user_id ) {
9588 $is_super_admin = false;
9589
9590 $user = self::_get_user_by_id( $user_id );
9591
9592 if ( $user instanceof FS_User && ! empty( $user->email ) ) {
9593 self::require_pluggable_essentials();
9594
9595 $wp_user = get_user_by( 'email', $user->email );
9596
9597 if ( $wp_user instanceof WP_User ) {
9598 $super_admins = get_super_admins();
9599 $is_super_admin = ( is_array( $super_admins ) && in_array( $wp_user->user_login, $super_admins ) );
9600 }
9601 }
9602
9603 return $is_super_admin;
9604 }
9605
9606 #----------------------------------------------------------------------------------
9607 #region Plans & Licensing
9608 #----------------------------------------------------------------------------------
9609
9610 /**
9611 * Check if running premium plugin code.
9612 *
9613 * @author Vova Feldman (@svovaf)
9614 * @since 1.0.5
9615 *
9616 * @return bool
9617 */
9618 function is_premium() {
9619 /**
9620 * `$this->_plugin` will be `false` when `is_activation_mode` calls this method directly from the
9621 * `_register_hooks` method.
9622 *
9623 * @author Leo Fajardo (@leorw)
9624 * @since 2.2.3
9625 */
9626 return is_object( $this->_plugin ) ?
9627 $this->_plugin->is_premium :
9628 false;
9629 }
9630
9631 /**
9632 * Get site's plan ID.
9633 *
9634 * @author Vova Feldman (@svovaf)
9635 * @since 1.0.2
9636 *
9637 * @return number
9638 */
9639 function get_plan_id() {
9640 return $this->_site->plan_id;
9641 }
9642
9643 /**
9644 * Get site's plan title.
9645 *
9646 * @author Vova Feldman (@svovaf)
9647 * @since 1.0.2
9648 *
9649 * @return string
9650 */
9651 function get_plan_title() {
9652 $plan = $this->get_plan();
9653
9654 return is_object( $plan ) ? $plan->title : 'PLAN_TITLE';
9655 }
9656
9657 /**
9658 * Get site's plan name.
9659 *
9660 * @author Vova Feldman (@svovaf)
9661 * @since 2.0.0
9662 *
9663 * @return string
9664 */
9665 function get_plan_name() {
9666 $plan = $this->get_plan();
9667
9668 return is_object( $plan ) ? $plan->name : 'PLAN_NAME';
9669 }
9670
9671 /**
9672 * @author Vova Feldman (@svovaf)
9673 * @since 1.0.9
9674 *
9675 * @return FS_Plugin_Plan|false
9676 */
9677 function get_plan() {
9678 if ( ! is_object( $this->_site ) ) {
9679 return false;
9680 }
9681
9682 return FS_Plugin_Plan::is_valid_id( $this->_site->plan_id ) ?
9683 $this->_get_plan_by_id( $this->_site->plan_id ) :
9684 false;
9685 }
9686
9687 /**
9688 * @author Vova Feldman (@svovaf)
9689 * @since 1.0.3
9690 *
9691 * @return bool
9692 */
9693 function is_trial() {
9694 $this->_logger->entrance();
9695
9696 if ( ! $this->is_registered() || ! is_object( $this->_site ) ) {
9697 return false;
9698 }
9699
9700 return $this->_site->is_trial();
9701 }
9702
9703 /**
9704 * Check if currently in a trial with payment method (credit card or paypal).
9705 *
9706 * @author Vova Feldman (@svovaf)
9707 * @since 1.1.7
9708 *
9709 * @return bool
9710 */
9711 function is_paid_trial() {
9712 $this->_logger->entrance();
9713
9714 if ( ! $this->is_trial() ) {
9715 return false;
9716 }
9717
9718 return $this->has_active_valid_license() && ( $this->_site->trial_plan_id == $this->_license->plan_id );
9719 }
9720
9721 /**
9722 * Check if trial already utilized.
9723 *
9724 * @since 1.0.9
9725 *
9726 * @return bool
9727 */
9728 function is_trial_utilized() {
9729 $this->_logger->entrance();
9730
9731 if ( ! $this->is_registered() ) {
9732 return false;
9733 }
9734
9735 return $this->_site->is_trial_utilized();
9736 }
9737
9738 /**
9739 * Get trial plan information (if in trial).
9740 *
9741 * @author Vova Feldman (@svovaf)
9742 * @since 1.0.9
9743 *
9744 * @return bool|FS_Plugin_Plan
9745 */
9746 function get_trial_plan() {
9747 $this->_logger->entrance();
9748
9749 if ( ! $this->is_trial() ) {
9750 return false;
9751 }
9752
9753 // Try to load plan from local cache.
9754 $trial_plan = $this->_get_plan_by_id( $this->_site->trial_plan_id );
9755
9756 if ( ! is_object( $trial_plan ) ) {
9757 $trial_plan = $this->_fetch_site_plan( $this->_site->trial_plan_id );
9758
9759 /**
9760 * If managed to fetch the plan, add it to the plans collection.
9761 */
9762 if ( $trial_plan instanceof FS_Plugin_Plan ) {
9763 if ( ! is_array( $this->_plans ) ) {
9764 $this->_plans = array();
9765 }
9766
9767 $this->_plans[] = $trial_plan;
9768 $this->_store_plans();
9769 }
9770 }
9771
9772 if ( $trial_plan instanceof FS_Plugin_Plan ) {
9773 return $trial_plan;
9774 }
9775
9776 /**
9777 * If for some reason failed to get the trial plan, fallback to a dummy name and title.
9778 */
9779 $trial_plan = new FS_Plugin_Plan();
9780 $trial_plan->id = $this->_site->trial_plan_id;
9781 $trial_plan->name = 'pro';
9782 $trial_plan->title = 'Pro';
9783
9784 return $trial_plan;
9785 }
9786
9787 /**
9788 * Check if the user has an activate, non-expired license on current plugin's install.
9789 *
9790 * @since 1.0.9
9791 *
9792 * @return bool
9793 */
9794 function is_paying() {
9795 $this->_logger->entrance();
9796
9797 if ( ! $this->is_registered() ) {
9798 return false;
9799 }
9800
9801 if ( ! $this->has_paid_plan() ) {
9802 return false;
9803 }
9804
9805 return (
9806 ! $this->is_trial() &&
9807 'free' !== $this->get_plan_name() &&
9808 $this->has_active_valid_license()
9809 );
9810 }
9811
9812 /**
9813 * @author Vova Feldman (@svovaf)
9814 * @since 1.0.4
9815 *
9816 * @return bool
9817 */
9818 function is_free_plan() {
9819 if ( ! $this->is_registered() ) {
9820 return true;
9821 }
9822
9823 if ( ! $this->has_paid_plan() ) {
9824 return true;
9825 }
9826
9827 return (
9828 'free' === $this->get_plan_name() ||
9829 ! $this->has_features_enabled_license()
9830 );
9831 }
9832
9833 /**
9834 * @author Vova Feldman (@svovaf)
9835 * @since 1.0.5
9836 *
9837 * @return bool
9838 */
9839 function _has_premium_license() {
9840 $this->_logger->entrance();
9841
9842 $premium_license = $this->_get_available_premium_license();
9843
9844 return ( false !== $premium_license );
9845 }
9846
9847 /**
9848 * Check if user has any licenses associated with the plugin (including expired or blocking).
9849 *
9850 * @author Vova Feldman (@svovaf)
9851 * @since 1.1.7.3
9852 *
9853 * @return bool
9854 */
9855 function has_any_license() {
9856 return is_array( $this->_licenses ) && ( 0 < count( $this->_licenses ) );
9857 }
9858
9859 /**
9860 * @author Vova Feldman (@svovaf)
9861 * @since 1.0.5
9862 *
9863 * @param bool|null $is_localhost
9864 *
9865 * @return FS_Plugin_License|false
9866 */
9867 function _get_available_premium_license( $is_localhost = null ) {
9868 $this->_logger->entrance();
9869
9870 $licenses = $this->get_available_premium_licenses( $is_localhost );
9871 if ( ! empty( $licenses ) ) {
9872 return $licenses[0];
9873 }
9874
9875 return false;
9876 }
9877
9878 /**
9879 * @author Vova Feldman (@svovaf)
9880 * @since 1.0.5
9881 *
9882 * @param bool|null $is_localhost
9883 *
9884 * @return FS_Plugin_License[]
9885 */
9886 function get_available_premium_licenses( $is_localhost = null ) {
9887 $this->_logger->entrance();
9888
9889 $licenses = array();
9890 if ( ! $this->has_paid_plan() ) {
9891 return $licenses;
9892 }
9893
9894 if ( is_array( $this->_licenses ) ) {
9895 foreach ( $this->_licenses as $license ) {
9896 if ( ! $license->can_activate( $is_localhost ) ) {
9897 continue;
9898 }
9899
9900 $licenses[] = $license;
9901 }
9902 }
9903
9904 return $licenses;
9905 }
9906
9907 /**
9908 * Sync local plugin plans with remote server.
9909 *
9910 * 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).
9911 *
9912 * @author Vova Feldman (@svovaf)
9913 * @since 1.0.5
9914 *
9915 * @return FS_Plugin_Plan[]|object
9916 */
9917 function _sync_plans() {
9918 $plans = $this->_fetch_plugin_plans();
9919
9920 if ( $this->is_array_instanceof( $plans, 'FS_Plugin_Plan' ) ) {
9921 $plans_map = array();
9922 foreach ( $plans as $plan ) {
9923 $plans_map[ $plan->id ] = true;
9924 }
9925
9926 $plans_ids_to_keep = $this->get_plans_ids_associated_with_installs();
9927
9928 foreach ( $plans_ids_to_keep as $plan_id ) {
9929 if ( isset( $plans_map[ $plan_id ] ) ) {
9930 continue;
9931 }
9932
9933 $missing_plan = self::_get_plan_by_id( $plan_id );
9934
9935 if ( is_object( $missing_plan ) ) {
9936 $plans[] = $missing_plan;
9937 }
9938 }
9939
9940 $this->_plans = $plans;
9941 $this->_store_plans();
9942 }
9943
9944 $this->do_action( 'after_plans_sync', $plans );
9945
9946 return $this->_plans;
9947 }
9948
9949 /**
9950 * Check if specified plan exists locally. If not, fetch it and store it.
9951 *
9952 * @author Vova Feldman (@svovaf)
9953 * @since 2.0.0
9954 *
9955 * @param number $plan_id
9956 *
9957 * @return \FS_Plugin_Plan|object The plan entity or the API error object on failure.
9958 */
9959 private function sync_plan_if_not_exist( $plan_id ) {
9960 $plan = self::_get_plan_by_id( $plan_id );
9961
9962 if ( is_object( $plan ) ) {
9963 // Plan already exists.
9964 return $plan;
9965 }
9966
9967 $plan = $this->fetch_plan_by_id( $plan_id );
9968
9969 if ( $plan instanceof FS_Plugin_Plan ) {
9970 $this->_plans[] = $plan;
9971 $this->_store_plans();
9972
9973 return $plan;
9974 }
9975
9976 return $plan;
9977 }
9978
9979 /**
9980 * Check if specified license exists locally. If not, fetch it and store it.
9981 *
9982 * @author Vova Feldman (@svovaf)
9983 * @since 2.0.0
9984 *
9985 * @param number $license_id
9986 * @param string $license_key
9987 *
9988 * @return \FS_Plugin_Plan|object The plan entity or the API error object on failure.
9989 */
9990 private function sync_license_if_not_exist( $license_id, $license_key ) {
9991 $license = $this->_get_license_by_id( $license_id );
9992
9993 if ( is_object( $license ) ) {
9994 // License already exists.
9995 return $license;
9996 }
9997
9998 $license = $this->fetch_license_by_key( $license_id, $license_key );
9999
10000 if ( $license instanceof FS_Plugin_License ) {
10001 $this->_licenses[] = $license;
10002 $this->_license = $license;
10003 $this->_store_licenses();
10004
10005 return $license;
10006 }
10007
10008 return $license;
10009 }
10010
10011 /**
10012 * Get a collection of unique plan IDs that are associated with any installs in the network.
10013 *
10014 * @author Leo Fajardo (@leorw)
10015 * @since 2.0.0
10016 *
10017 * @return number[]
10018 */
10019 private function get_plans_ids_associated_with_installs() {
10020 if ( ! $this->_is_network_active ) {
10021 if ( ! is_object( $this->_site ) ||
10022 ! FS_Plugin_Plan::is_valid_id( $this->_site->plan_id )
10023 ) {
10024 return array();
10025 }
10026
10027 return array( $this->_site->plan_id );
10028 }
10029
10030 $plan_ids = array();
10031 $sites = self::get_sites();
10032 foreach ( $sites as $site ) {
10033 $blog_id = self::get_site_blog_id( $site );
10034 $install = $this->get_install_by_blog_id( $blog_id );
10035
10036 if ( ! is_object( $install ) ||
10037 ! FS_Plugin_Plan::is_valid_id( $install->plan_id )
10038 ) {
10039 continue;
10040 }
10041
10042 $plan_ids[ $install->plan_id ] = true;
10043 }
10044
10045 return array_keys( $plan_ids );
10046 }
10047
10048 /**
10049 * Get a collection of unique license IDs that are associated with any installs in the network.
10050 *
10051 * @author Leo Fajardo (@leorw)
10052 * @since 2.0.0
10053 *
10054 * @return number[]
10055 */
10056 private function get_license_ids_associated_with_installs() {
10057 if ( ! $this->_is_network_active ) {
10058 if ( ! is_object( $this->_site ) ||
10059 ! FS_Plugin_License::is_valid_id( $this->_site->license_id )
10060 ) {
10061 return array();
10062 }
10063
10064 return array( $this->_site->license_id );
10065 }
10066
10067 $license_ids = array();
10068 $sites = self::get_sites();
10069 foreach ( $sites as $site ) {
10070 $blog_id = self::get_site_blog_id( $site );
10071 $install = $this->get_install_by_blog_id( $blog_id );
10072
10073 if ( ! is_object( $install ) ||
10074 ! FS_Plugin_License::is_valid_id( $install->license_id )
10075 ) {
10076 continue;
10077 }
10078
10079 $license_ids[ $install->license_id ] = true;
10080 }
10081
10082 return array_keys( $license_ids );
10083 }
10084
10085 /**
10086 * @author Vova Feldman (@svovaf)
10087 * @since 1.0.5
10088 *
10089 * @param number $id
10090 *
10091 * @return FS_Plugin_Plan|false
10092 */
10093 function _get_plan_by_id( $id ) {
10094 $this->_logger->entrance();
10095
10096 if ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) {
10097 $this->_sync_plans();
10098 }
10099
10100 foreach ( $this->_plans as $plan ) {
10101 if ( $id == $plan->id ) {
10102 return $plan;
10103 }
10104 }
10105
10106 return false;
10107 }
10108
10109 /**
10110 * @author Vova Feldman (@svovaf)
10111 * @since 1.1.8.1
10112 *
10113 * @param string $name
10114 *
10115 * @return FS_Plugin_Plan|false
10116 */
10117 private function get_plan_by_name( $name ) {
10118 $this->_logger->entrance();
10119
10120 if ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) {
10121 $this->_sync_plans();
10122 }
10123
10124 foreach ( $this->_plans as $plan ) {
10125 if ( $name == $plan->name ) {
10126 return $plan;
10127 }
10128 }
10129
10130 return false;
10131 }
10132
10133 /**
10134 * Sync local licenses with remote server.
10135 *
10136 * @author Vova Feldman (@svovaf)
10137 * @since 1.0.6
10138 *
10139 * @param number|bool $site_license_id
10140 * @param number|null $blog_id
10141 *
10142 * @return FS_Plugin_License[]|object
10143 */
10144 function _sync_licenses( $site_license_id = false, $blog_id = null ) {
10145 $this->_logger->entrance();
10146
10147 $is_network_admin = fs_is_network_admin();
10148
10149 if ( $is_network_admin && is_null( $blog_id ) ) {
10150 $all_licenses = self::get_all_licenses( $this->_module_id );
10151 } else {
10152 $all_licenses = $this->get_user_licenses( $this->_user->id );
10153 }
10154
10155 $foreign_licenses = array(
10156 'ids' => array(),
10157 'license_keys' => array()
10158 );
10159
10160 $all_licenses_map = array();
10161 foreach ( $all_licenses as $license ) {
10162 $all_licenses_map[ $license->id ] = true;
10163 if ( $license->user_id == $this->_user->id || $license->id == $site_license_id ) {
10164 continue;
10165 }
10166
10167 $foreign_licenses['ids'][] = $license->id;
10168 $foreign_licenses['license_keys'][] = $license->secret_key;
10169 }
10170
10171 if ( empty( $foreign_licenses['ids'] ) ) {
10172 $foreign_licenses = array();
10173 }
10174
10175 $licenses = $this->_fetch_licenses( false, $site_license_id, $foreign_licenses, $blog_id );
10176
10177 if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) {
10178 $licenses_map = array();
10179 foreach ( $licenses as $license ) {
10180 $licenses_map[ $license->id ] = true;
10181 }
10182
10183 // $license_ids_to_keep = $this->get_license_ids_associated_with_installs();
10184 // foreach ( $license_ids_to_keep as $license_id ) {
10185 // if ( isset( $licenses_map[ $license_id ] ) ) {
10186 // continue;
10187 // }
10188 //
10189 // $missing_license = self::_get_license_by_id( $license_id, false );
10190 // if ( is_object( $missing_license ) ) {
10191 // $licenses[] = $missing_license;
10192 // $licenses_map[ $missing_license->id ] = true;
10193 // }
10194 // }
10195
10196 $user_license_ids = $this->get_user_linked_license_ids( $this->_user->id );
10197
10198 foreach ( $user_license_ids as $key => $license_id ) {
10199 if ( ! isset( $licenses_map[ $license_id ] ) ) {
10200 // Remove access to licenses that no longer exist.
10201 unset( $user_license_ids[ $key ] );
10202 }
10203 }
10204
10205 if ( ! empty( $user_license_ids ) ) {
10206 foreach ( $licenses_map as $license_id => $value ) {
10207 if ( ! isset( $all_licenses_map[ $license_id ] ) ) {
10208 // Associate new licenses with the user who triggered the license syncing.
10209 $user_license_ids[] = $license_id;
10210 }
10211 }
10212
10213 $user_license_ids = array_unique( $user_license_ids );
10214 } else {
10215 $user_license_ids = array_keys( $licenses_map );
10216 }
10217
10218 if ( ! $is_network_admin || ! is_null( $blog_id ) ) {
10219 $user_licenses = array();
10220 foreach ( $licenses as $license ) {
10221 if ( ! in_array( $license->id, $user_license_ids ) ) {
10222 continue;
10223 }
10224
10225 $user_licenses[] = $license;
10226 }
10227
10228 $this->_licenses = $user_licenses;
10229 } else {
10230 $this->_licenses = $licenses;
10231 }
10232
10233 $this->set_user_linked_license_ids( $this->_user->id, $user_license_ids );
10234
10235 $this->_store_licenses( true, $this->_module_id, $licenses );
10236 }
10237
10238 // Update current license.
10239 if ( is_object( $this->_license ) ) {
10240 $this->_license = $this->_get_license_by_id( $this->_license->id );
10241 }
10242
10243 return $this->_licenses;
10244 }
10245
10246 /**
10247 * @author Vova Feldman (@svovaf)
10248 * @since 1.0.5
10249 *
10250 * @param number $id
10251 * @param bool $sync_licenses
10252 *
10253 * @return FS_Plugin_License|false
10254 */
10255 function _get_license_by_id( $id, $sync_licenses = true ) {
10256 $this->_logger->entrance();
10257
10258 if ( ! FS_Plugin_License::is_valid_id( $id ) ) {
10259 return false;
10260 }
10261
10262 /**
10263 * When running from the network level admin and opted-in from the network,
10264 * check if the license exists in the network user licenses collection.
10265 *
10266 * @author Vova Feldman (@svovaf)
10267 * @since 2.0.0
10268 */
10269 if ( fs_is_network_admin() &&
10270 $this->is_network_registered() &&
10271 ( ! is_object( $this->_user ) || $this->_storage->network_user_id != $this->_user->id )
10272 ) {
10273 $licenses = $this->get_user_licenses( $this->_storage->network_user_id );
10274
10275 foreach ( $licenses as $license ) {
10276 if ( $id == $license->id ) {
10277 return $license;
10278 }
10279 }
10280 }
10281
10282 if ( ! $this->has_any_license() && $sync_licenses ) {
10283 $this->_sync_licenses( $id );
10284 }
10285
10286 if ( is_array( $this->_licenses ) ) {
10287 foreach ( $this->_licenses as $license ) {
10288 if ( $id == $license->id ) {
10289 return $license;
10290 }
10291 }
10292 }
10293
10294 return false;
10295 }
10296
10297 /**
10298 * 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.
10299 *
10300 * @author Vova Feldman (@svovaf)
10301 * @since 2.0.0
10302 *
10303 * @param number $id
10304 *
10305 * @return FS_Plugin_License
10306 */
10307 private function get_license_by_id( $id ) {
10308 $licenses = self::get_all_licenses( $this->_module_id );
10309
10310 if ( is_array( $licenses ) && ! empty( $licenses ) ) {
10311 foreach ( $licenses as $license ) {
10312 if ( $id == $license->id ) {
10313 return $license;
10314 }
10315 }
10316 }
10317
10318 return null;
10319 }
10320
10321 /**
10322 * Synchronize the site's context license by fetching the license form the API and updating the local data with it.
10323 *
10324 * @author Vova Feldman (@svovaf)
10325 * @since 2.0.0
10326 *
10327 * @return \FS_Plugin_License|mixed
10328 */
10329 private function sync_site_license() {
10330 $api = $this->get_api_user_scope();
10331
10332 $result = $api->get( "/licenses/{$this->_license->id}.json?license_key=" . urlencode( $this->_license->secret_key ), true );
10333
10334 if ( ! $this->is_api_result_entity( $result ) ) {
10335 return $result;
10336 }
10337
10338 $license = $this->_update_site_license( new FS_Plugin_License( $result ) );
10339 $this->_store_licenses();
10340
10341 return $license;
10342 }
10343
10344 /**
10345 * Get all user's available licenses for the current module.
10346 *
10347 * @author Vova Feldman (@svovaf)
10348 * @since 2.0.0
10349 *
10350 * @param number $user_id
10351 *
10352 * @return FS_Plugin_License[]
10353 */
10354 private function get_user_licenses( $user_id ) {
10355 $all_licenses = self::get_all_licenses( $this->_module_id );
10356 if ( empty( $all_licenses ) ) {
10357 return array();
10358 }
10359
10360 $user_license_ids = $this->get_user_linked_license_ids( $user_id );
10361 if ( empty( $user_license_ids ) ) {
10362 return array();
10363 }
10364
10365 $licenses = array();
10366 foreach ( $all_licenses as $license ) {
10367 if ( in_array( $license->id, $user_license_ids ) ) {
10368 $licenses[] = $license;
10369 }
10370 }
10371
10372 return $licenses;
10373 }
10374
10375 /**
10376 * Checks if the context license is network activated except on the given blog ID.
10377 *
10378 * @author Vova Feldman (@svovaf)
10379 * @since 2.0.0
10380 *
10381 * @param int $except_blog_id
10382 *
10383 * @return bool
10384 */
10385 private function is_license_network_active( $except_blog_id = 0 ) {
10386 $this->_logger->entrance();
10387
10388 if ( ! is_object( $this->_license ) ) {
10389 return false;
10390 }
10391
10392 $sites = self::get_sites();
10393
10394 if ( $this->_license->total_activations() < ( count( $sites ) - 1 ) ) {
10395 // There are more sites than the number of activations, so license cannot be network activated.
10396 return false;
10397 }
10398
10399 foreach ( $sites as $site ) {
10400 $blog_id = self::get_site_blog_id( $site );
10401
10402 if ( $except_blog_id == $blog_id ) {
10403 // Skip excluded blog.
10404 continue;
10405 }
10406
10407 $install = $this->get_install_by_blog_id( $blog_id );
10408
10409 if ( is_object( $install ) && $install->license_id != $this->_license->id ) {
10410 return false;
10411 }
10412 }
10413
10414 return true;
10415 }
10416
10417 /**
10418 * 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.
10419 *
10420 * Notice: On success, this method will also update the license activations counters (without updating the license in the storage).
10421 *
10422 * @author Vova Feldman (@svovaf)
10423 * @since 2.0.0
10424 *
10425 * @param \FS_User $user
10426 * @param \FS_Plugin_License $license
10427 *
10428 * @return bool
10429 */
10430 private function try_activate_license_on_network( FS_User $user, FS_Plugin_License $license ) {
10431 $this->_logger->entrance();
10432
10433 $result = $this->can_activate_license_on_network( $license );
10434
10435 if ( false === $result ) {
10436 return false;
10437 }
10438
10439 $installs_without_license = $result['installs'];
10440 if ( ! empty( $installs_without_license ) ) {
10441 $this->activate_license_on_many_installs( $user, $license->secret_key, $installs_without_license );
10442 }
10443
10444 $disconnected_site_ids = $result['sites'];
10445 if ( ! empty( $disconnected_site_ids ) ) {
10446 $this->activate_license_on_many_sites( $user, $license->secret_key, $disconnected_site_ids );
10447 }
10448
10449 $this->link_license_2_user( $license->id, $user->id );
10450
10451 // Sync license after activations.
10452 $license->activated += $result['production_count'];
10453 $license->activated_local += $result['localhost_count'];
10454
10455 // $this->_store_licenses()
10456
10457 return true;
10458 }
10459
10460 /**
10461 * Checks if the given license can be activated on the whole network.
10462 *
10463 * @author Vova Feldman (@svovaf)
10464 * @since 2.0.0
10465 *
10466 * @param \FS_Plugin_License $license
10467 *
10468 * @return false|array {
10469 * @type array[int]FS_Site $installs Blog ID to install map.
10470 * @type int[] $sites Non-connected blog IDs.
10471 * @type int $production_count Production sites count.
10472 * @type int $localhost_count Production sites count.
10473 * }
10474 */
10475 private function can_activate_license_on_network( FS_Plugin_License $license ) {
10476 $sites = self::get_sites();
10477
10478 $production_count = 0;
10479 $localhost_count = 0;
10480
10481 $installs_without_license = array();
10482 $disconnected_site_ids = array();
10483
10484 foreach ( $sites as $site ) {
10485 $blog_id = self::get_site_blog_id( $site );
10486 $install = $this->get_install_by_blog_id( $blog_id );
10487
10488 if ( is_object( $install ) ) {
10489 if ( FS_Plugin_License::is_valid_id( $install->license_id ) ) {
10490 // License already activated on the install.
10491 continue;
10492 }
10493
10494 $url = $install->url;
10495
10496 $installs_without_license[ $blog_id ] = $install;
10497 } else {
10498 $url = is_object( $site ) ?
10499 $site->siteurl :
10500 get_site_url( $blog_id );
10501
10502 $disconnected_site_ids[] = $blog_id;
10503 }
10504
10505 if ( FS_Site::is_localhost_by_address( $url ) ) {
10506 $localhost_count ++;
10507 } else {
10508 $production_count ++;
10509 }
10510 }
10511
10512 if ( ! $license->can_activate_bulk( $production_count, $localhost_count ) ) {
10513 return false;
10514 }
10515
10516 return array(
10517 'installs' => $installs_without_license,
10518 'sites' => $disconnected_site_ids,
10519 'production_count' => $production_count,
10520 'localhost_count' => $localhost_count,
10521 );
10522 }
10523
10524 /**
10525 * Activate a given license on a collection of installs.
10526 *
10527 * @author Vova Feldman (@svovaf)
10528 * @since 2.0.0
10529 *
10530 * @param \FS_User $user
10531 * @param string $license_key
10532 * @param array $blog_2_install_map {
10533 * @key int Blog ID.
10534 * @value FS_Site Blog's associated install.
10535 * }
10536 *
10537 * @return mixed|true
10538 */
10539 private function activate_license_on_many_installs(
10540 FS_User $user,
10541 $license_key,
10542 array $blog_2_install_map
10543 ) {
10544 $params = array(
10545 array( 'license_key' => $this->apply_filters( 'license_key', $license_key ) )
10546 );
10547
10548 $install_2_blog_map = array();
10549 foreach ( $blog_2_install_map as $blog_id => $install ) {
10550 $params[] = array( 'id' => $install->id );
10551
10552 $install_2_blog_map[ $install->id ] = $blog_id;
10553 }
10554
10555 $result = $this->get_api_user_scope_by_user( $user )->call(
10556 "plugins/{$this->_plugin->id}/installs.json",
10557 'PUT',
10558 $params
10559 );
10560
10561 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
10562 return $result;
10563 }
10564
10565 foreach ( $result->installs as $r_install ) {
10566 $install = new FS_Site( $r_install );
10567 $install->is_disconnected = false;
10568
10569 // Update install.
10570 $this->_store_site(
10571 true,
10572 $install_2_blog_map[ $r_install->id ],
10573 $install
10574 );
10575 }
10576
10577 return true;
10578 }
10579
10580 /**
10581 * Activate a given license on a collection of blogs/sites that are not yet opted-in.
10582 *
10583 * @author Vova Feldman (@svovaf)
10584 * @since 2.0.0
10585 *
10586 * @param \FS_User $user
10587 * @param string $license_key
10588 * @param int[] $site_ids
10589 *
10590 * @return true|mixed True if successful, otherwise, the API result.
10591 */
10592 private function activate_license_on_many_sites(
10593 FS_User $user,
10594 $license_key,
10595 array $site_ids
10596 ) {
10597 $sites = array();
10598 foreach ( $site_ids as $site_id ) {
10599 $sites[] = $this->get_site_info( array( 'blog_id' => $site_id ) );
10600 }
10601
10602 // Install the plugin.
10603 $result = $this->create_installs_with_user(
10604 $user,
10605 $license_key,
10606 false,
10607 $sites,
10608 false,
10609 true
10610 );
10611
10612 if ( ! $this->is_api_result_entity( $result ) &&
10613 ! $this->is_api_result_object( $result, 'installs' )
10614 ) {
10615 return $result;
10616 }
10617
10618 $installs = array();
10619 foreach ( $result->installs as $install ) {
10620 $installs[] = new FS_Site( $install );
10621 }
10622
10623 // Map site addresses to their blog IDs.
10624 $address_to_blog_map = $this->get_address_to_blog_map();
10625
10626 $first_blog_id = null;
10627
10628 foreach ( $installs as $install ) {
10629 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
10630 $blog_id = $address_to_blog_map[ $address ];
10631
10632 $this->_store_site( true, $blog_id, $install );
10633
10634 $this->reset_anonymous_mode( $blog_id );
10635
10636 if ( is_null( $first_blog_id ) ) {
10637 $first_blog_id = $blog_id;
10638 }
10639 }
10640
10641 if ( ! FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ) {
10642 $this->_storage->network_install_blog_id = $first_blog_id;
10643 }
10644
10645 return true;
10646 }
10647
10648 /**
10649 * Sync site's license with user licenses.
10650 *
10651 * @author Vova Feldman (@svovaf)
10652 * @since 1.0.6
10653 *
10654 * @param FS_Plugin_License|null $new_license
10655 *
10656 * @return FS_Plugin_License|null
10657 */
10658 function _update_site_license( $new_license ) {
10659 $this->_logger->entrance();
10660
10661 $this->_license = $new_license;
10662
10663 if ( ! is_object( $new_license ) ) {
10664 $this->_site->license_id = null;
10665 $this->_sync_site_subscription( null );
10666
10667 return $this->_license;
10668 }
10669
10670 $this->_site->license_id = $this->_license->id;
10671
10672 if ( ! is_array( $this->_licenses ) ) {
10673 $this->_licenses = array();
10674 }
10675
10676 $is_license_found = false;
10677 for ( $i = 0, $len = count( $this->_licenses ); $i < $len; $i ++ ) {
10678 if ( $new_license->id == $this->_licenses[ $i ]->id ) {
10679 $this->_licenses[ $i ] = $new_license;
10680
10681 $is_license_found = true;
10682 break;
10683 }
10684 }
10685
10686 // If new license just append.
10687 if ( ! $is_license_found ) {
10688 $this->_licenses[] = $new_license;
10689 }
10690
10691 $this->_sync_site_subscription( $new_license );
10692
10693 return $this->_license;
10694 }
10695
10696 /**
10697 * Sync site's subscription.
10698 *
10699 * @author Vova Feldman (@svovaf)
10700 * @since 1.0.9
10701 *
10702 * @param FS_Plugin_License|null $license
10703 *
10704 * @return bool|\FS_Subscription
10705 */
10706 private function _sync_site_subscription( $license ) {
10707 if ( ! is_object( $license ) ) {
10708 $this->delete_unused_subscriptions();
10709
10710 return false;
10711 }
10712
10713 // Load subscription details if not lifetime.
10714 $subscription = $license->is_lifetime() ?
10715 false :
10716 $this->_fetch_site_license_subscription();
10717
10718 if ( is_object( $subscription ) && ! isset( $subscription->error ) ) {
10719 $this->store_subscription( $subscription );
10720 } else {
10721 $this->delete_unused_subscriptions();
10722 }
10723
10724 return $subscription;
10725 }
10726
10727 /**
10728 * @author Vova Feldman (@svovaf)
10729 * @since 1.0.6
10730 *
10731 * @return bool|\FS_Plugin_License
10732 */
10733 function _get_license() {
10734 if ( ! fs_is_network_admin() || is_object( $this->_license ) ) {
10735 return $this->_license;
10736 }
10737
10738 return $this->_get_available_premium_license();
10739 }
10740
10741 /**
10742 * @param number $license_id
10743 *
10744 * @return null|\FS_Subscription
10745 */
10746 function _get_subscription( $license_id ) {
10747 if ( ! isset( $this->_storage->subscriptions ) ||
10748 empty( $this->_storage->subscriptions )
10749 ) {
10750 return null;
10751 }
10752
10753 foreach ( $this->_storage->subscriptions as $subscription ) {
10754 if ( $subscription->license_id == $license_id ) {
10755 return $subscription;
10756 }
10757 }
10758
10759 return null;
10760 }
10761
10762 /**
10763 * @author Leo Fajardo (@leorw)
10764 * @since 2.0.0
10765 *
10766 * @param FS_Subscription $subscription
10767 */
10768 function store_subscription( FS_Subscription $subscription ) {
10769 if ( ! isset( $this->_storage->subscriptions ) ) {
10770 $this->_storage->subscriptions = array();
10771 }
10772
10773 if ( empty( $this->_storage->subscriptions ) || ! is_multisite() ) {
10774 $this->_storage->subscriptions = array( $subscription );
10775
10776 return;
10777 }
10778
10779 $subscriptions = $this->_storage->subscriptions;
10780
10781 $updated_subscription = false;
10782 foreach ( $subscriptions as $key => $existing_subscription ) {
10783 if ( $existing_subscription->id == $subscription->id ) {
10784 $subscriptions[ $key ] = $subscription;
10785 $updated_subscription = true;
10786 break;
10787 }
10788 }
10789
10790 if ( ! $updated_subscription ) {
10791 $subscriptions[] = $subscription;
10792 }
10793
10794 $this->_storage->subscriptions = $subscriptions;
10795 }
10796
10797 /**
10798 * @author Leo Fajardo (@leorw)
10799 * @since 2.0.0
10800 */
10801 function delete_unused_subscriptions() {
10802 if ( ! isset( $this->_storage->subscriptions ) ||
10803 empty( $this->_storage->subscriptions ) ||
10804 // Clean up only if there are already at least 3 subscriptions.
10805 ( count( $this->_storage->subscriptions ) < 3 )
10806 ) {
10807 return;
10808 }
10809
10810 if ( ! is_multisite() ) {
10811 // If not multisite, there should only be 1 subscription, so just clear the array.
10812 $this->_storage->subscriptions = array();
10813
10814 return;
10815 }
10816
10817 $subscriptions_to_keep_by_license_id_map = array();
10818 $sites = self::get_sites();
10819 foreach ( $sites as $site ) {
10820 $blog_id = self::get_site_blog_id( $site );
10821 $install = $this->get_install_by_blog_id( $blog_id );
10822
10823 if ( ! is_object( $install ) ||
10824 ! FS_Plugin_License::is_valid_id( $install->license_id )
10825 ) {
10826 continue;
10827 }
10828
10829 $subscriptions_to_keep_by_license_id_map[ $install->license_id ] = true;
10830 }
10831
10832 if ( empty( $subscriptions_to_keep_by_license_id_map ) ) {
10833 $this->_storage->subscriptions = array();
10834
10835 return;
10836 }
10837
10838 foreach ( $this->_storage->subscriptions as $key => $subscription ) {
10839 if ( ! isset( $subscriptions_to_keep_by_license_id_map[ $subscription->license_id ] ) ) {
10840 unset( $this->_storage->subscriptions[ $key ] );
10841 }
10842 }
10843 }
10844
10845 /**
10846 * @author Vova Feldman (@svovaf)
10847 * @since 1.0.2
10848 *
10849 * @param string $plan Plan name
10850 * @param bool $exact If true, looks for exact plan. If false, also check "higher" plans.
10851 *
10852 * @return bool
10853 */
10854 function is_plan( $plan, $exact = false ) {
10855 $this->_logger->entrance();
10856
10857 if ( ! $this->is_registered() ) {
10858 return false;
10859 }
10860
10861 $plan = strtolower( $plan );
10862
10863 $current_plan_name = $this->get_plan_name();
10864
10865 if ( $current_plan_name === $plan ) {
10866 // Exact plan.
10867 return true;
10868 } else if ( $exact ) {
10869 // Required exact, but plans are different.
10870 return false;
10871 }
10872
10873 $current_plan_order = - 1;
10874 $required_plan_order = - 1;
10875 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
10876 if ( $plan === $this->_plans[ $i ]->name ) {
10877 $required_plan_order = $i;
10878 } else if ( $current_plan_name === $this->_plans[ $i ]->name ) {
10879 $current_plan_order = $i;
10880 }
10881 }
10882
10883 return ( $current_plan_order > $required_plan_order );
10884 }
10885
10886 /**
10887 * Check if module has only one plan.
10888 *
10889 * @author Vova Feldman (@svovaf)
10890 * @since 1.2.1.7
10891 *
10892 * @return bool
10893 */
10894 function is_single_plan() {
10895 $this->_logger->entrance();
10896
10897 if ( ! $this->is_registered() ||
10898 ! is_array( $this->_plans ) ||
10899 0 === count( $this->_plans )
10900 ) {
10901 return true;
10902 }
10903
10904 return ( 1 === count( $this->_plans ) );
10905 }
10906
10907 /**
10908 * Check if plan based on trial. If not in trial mode, should return false.
10909 *
10910 * @since 1.0.9
10911 *
10912 * @param string $plan Plan name
10913 * @param bool $exact If true, looks for exact plan. If false, also check "higher" plans.
10914 *
10915 * @return bool
10916 */
10917 function is_trial_plan( $plan, $exact = false ) {
10918 $this->_logger->entrance();
10919
10920 if ( ! $this->is_registered() ) {
10921 return false;
10922 }
10923
10924 if ( ! $this->is_trial() ) {
10925 return false;
10926 }
10927
10928 $trial_plan = $this->get_trial_plan();
10929
10930 if ( $trial_plan->name === $plan ) {
10931 // Exact plan.
10932 return true;
10933 } else if ( $exact ) {
10934 // Required exact, but plans are different.
10935 return false;
10936 }
10937
10938 $current_plan_order = - 1;
10939 $required_plan_order = - 1;
10940 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
10941 if ( $plan === $this->_plans[ $i ]->name ) {
10942 $required_plan_order = $i;
10943 } else if ( $trial_plan->name === $this->_plans[ $i ]->name ) {
10944 $current_plan_order = $i;
10945 }
10946 }
10947
10948 return ( $current_plan_order > $required_plan_order );
10949 }
10950
10951 /**
10952 * Check if plugin has any paid plans.
10953 *
10954 * @author Vova Feldman (@svovaf)
10955 * @since 1.0.7
10956 *
10957 * @return bool
10958 */
10959 function has_paid_plan() {
10960 return $this->_has_paid_plans ||
10961 FS_Plan_Manager::instance()->has_paid_plan( $this->_plans );
10962 }
10963
10964 /**
10965 * Check if plugin has any plan with a trail.
10966 *
10967 * @author Vova Feldman (@svovaf)
10968 * @since 1.0.9
10969 *
10970 * @return bool
10971 */
10972 function has_trial_plan() {
10973 /**
10974 * @author Vova Feldman(@svovaf)
10975 * @since 1.2.1.5
10976 *
10977 * Allow setting a trial from the SDK without calling the API.
10978 * But, if the user did opt-in, continue using the real data from the API.
10979 */
10980 if ( $this->_trial_days >= 0 ) {
10981 return true;
10982 }
10983
10984 return $this->_storage->get( 'has_trial_plan', false );
10985 }
10986
10987 /**
10988 * Check if plugin has any free plan, or is it premium only.
10989 *
10990 * Note: If no plans configured, assume plugin is free.
10991 *
10992 * @author Vova Feldman (@svovaf)
10993 * @since 1.0.7
10994 *
10995 * @return bool
10996 */
10997 function has_free_plan() {
10998 return ! $this->is_only_premium();
10999 }
11000
11001 /**
11002 * Displays a license activation dialog box when the user clicks on the "Activate License"
11003 * or "Change License" link on the plugins
11004 * page.
11005 *
11006 * @author Leo Fajardo (@leorw)
11007 * @since 1.1.9
11008 */
11009 function _add_license_activation_dialog_box() {
11010 $vars = array(
11011 'id' => $this->_module_id,
11012 );
11013
11014 fs_require_template( 'forms/license-activation.php', $vars );
11015 fs_require_template( 'forms/resend-key.php', $vars );
11016 }
11017
11018 /**
11019 * Displays a subscription cancellation dialog box when the user clicks on the "Deactivate License"
11020 * link on the "Account" page or deactivates a plugin and there's an active subscription that is
11021 * either associated with a non-lifetime single-site license or non-lifetime multisite license that
11022 * is only activated on a single production site.
11023 *
11024 * @author Leo Fajardo (@leorw)
11025 * @since 2.2.1
11026 *
11027 * @param bool $is_license_deactivation
11028 */
11029 function _maybe_add_subscription_cancellation_dialog_box( $is_license_deactivation = false ) {
11030 if ( fs_is_network_admin() ) {
11031 // Subscription cancellation dialog box is currently not supported for multisite networks.
11032 return;
11033 }
11034
11035 $license = $this->_get_license();
11036
11037 /**
11038 * 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.
11039 *
11040 * @author Leo Fajardo (@leorw) (Comment added by Vova Feldman @svovaf)
11041 * @since 2.2.1
11042 */
11043 if ( ! is_object( $license ) ||
11044 $license->is_lifetime() ||
11045 ( ! $license->is_single_site() && $license->activated > 1 )
11046 ) {
11047 return;
11048 }
11049
11050 /**
11051 * @var FS_Subscription $subscription
11052 */
11053 $subscription = $this->_get_subscription( $license->id );
11054 if ( ! is_object( $subscription ) || ! $subscription->is_active() ) {
11055 return;
11056 }
11057
11058 $vars = array(
11059 'id' => $this->_module_id,
11060 'license' => $license,
11061 'has_trial' => $this->is_paid_trial(),
11062 'is_license_deactivation' => $is_license_deactivation,
11063 );
11064
11065 fs_require_template( 'forms/subscription-cancellation.php', $vars );
11066 }
11067
11068 /**
11069 * @author Leo Fajardo (@leorw)
11070 * @since 2.0.2
11071 */
11072 function _add_premium_version_upgrade_selection_dialog_box() {
11073 $modules_update = get_site_transient( $this->is_theme() ? 'update_themes' : 'update_plugins' );
11074 if ( ! isset( $modules_update->response[ $this->_plugin_basename ] ) ) {
11075 return;
11076 }
11077
11078 $vars = array(
11079 'id' => $this->_module_id,
11080 'new_version' => is_object( $modules_update->response[ $this->_plugin_basename ] ) ?
11081 $modules_update->response[ $this->_plugin_basename ]->new_version :
11082 $modules_update->response[ $this->_plugin_basename ]['new_version']
11083 );
11084
11085 fs_require_template( 'forms/premium-versions-upgrade-metadata.php', $vars );
11086 fs_require_once_template( 'forms/premium-versions-upgrade-handler.php', $vars );
11087 }
11088
11089 /**
11090 * Displays the opt-out dialog box when the user clicks on the "Opt Out" link on the "Plugins"
11091 * page.
11092 *
11093 * @author Leo Fajardo (@leorw)
11094 * @since 1.2.1.5
11095 */
11096 function _add_optout_dialog() {
11097 if ( $this->is_theme() ) {
11098 $vars = null;
11099 fs_require_once_template( '/js/jquery.content-change.php', $vars );
11100 }
11101
11102 $vars = array( 'id' => $this->_module_id );
11103 fs_require_template( 'forms/optout.php', $vars );
11104 }
11105
11106 /**
11107 * Prepare page to include all required UI and logic for the license activation dialog.
11108 *
11109 * @author Vova Feldman (@svovaf)
11110 * @since 1.2.0
11111 */
11112 function _add_license_activation() {
11113 if ( ! $this->is_user_admin() ) {
11114 // Only admins can activate a license.
11115 return;
11116 }
11117
11118 if ( ! $this->has_paid_plan() ) {
11119 // Module doesn't have any paid plans.
11120 return;
11121 }
11122
11123 if ( ! $this->is_premium() ) {
11124 // Only add license activation logic to the premium version.
11125 return;
11126 }
11127
11128 // Add license activation link and AJAX request handler.
11129 if ( self::is_plugins_page() ) {
11130 /**
11131 * @since 1.2.0 Add license action link only on plugins page.
11132 */
11133 $this->_add_license_action_link();
11134 }
11135
11136 // Add license activation AJAX callback.
11137 $this->add_ajax_action( 'activate_license', array( &$this, '_activate_license_ajax_action' ) );
11138
11139 // Add resend license AJAX callback.
11140 $this->add_ajax_action( 'resend_license_key', array( &$this, '_resend_license_key_ajax_action' ) );
11141 }
11142
11143 /**
11144 * @author Leo Fajardo (@leorw)
11145 * @since 2.0.2
11146 */
11147 function _add_premium_version_upgrade_selection() {
11148 if ( ! $this->is_user_admin() ) {
11149 return;
11150 }
11151
11152 if ( ! $this->is_premium() || $this->has_any_active_valid_license() ) {
11153 // This is relevant only to the free versions and premium versions without an active license.
11154 return;
11155 }
11156
11157 if ( self::is_updates_page() || ( $this->is_plugin() && self::is_plugins_page() ) ) {
11158 $this->_add_premium_version_upgrade_selection_action();
11159 }
11160 }
11161
11162 /**
11163 * @author Leo Fajardo (@leorw)
11164 *
11165 * @since 1.1.9
11166 * @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).
11167 */
11168 function _activate_license_ajax_action() {
11169 $this->_logger->entrance();
11170
11171 $this->check_ajax_referer( 'activate_license' );
11172
11173 $license_key = trim( fs_request_get( 'license_key' ) );
11174
11175 if ( empty( $license_key ) ) {
11176 exit;
11177 }
11178
11179 $plugin_id = fs_request_get( 'module_id', '', 'post' );
11180 $fs = ( $plugin_id == $this->_module_id ) ?
11181 $this :
11182 $this->get_addon_instance( $plugin_id );
11183
11184 $error = false;
11185 $next_page = false;
11186
11187 $sites = fs_is_network_admin() ?
11188 fs_request_get( 'sites', array(), 'post' ) :
11189 array();
11190
11191 $blog_id = fs_request_get( 'blog_id' );
11192 $has_valid_blog_id = is_numeric( $blog_id );
11193
11194 if ( $fs->is_registered() ) {
11195 if ( fs_is_network_admin() && ! $has_valid_blog_id ) {
11196 // If no specific blog ID was provided, activate the license for all sites in the network.
11197 $blog_2_install_map = array();
11198 $site_ids = array();
11199
11200 foreach ( $sites as $site ) {
11201 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
11202 continue;
11203 }
11204
11205 $install = $this->get_install_by_blog_id( $site['blog_id'] );
11206
11207 if ( is_object( $install ) ) {
11208 $blog_2_install_map[ $site['blog_id'] ] = $install;
11209 } else {
11210 $site_ids[] = $site['blog_id'];
11211 }
11212 }
11213
11214 $user = $this->get_current_or_network_user();
11215
11216 if ( ! empty( $blog_2_install_map ) ) {
11217 $result = $this->activate_license_on_many_installs( $user, $license_key, $blog_2_install_map );
11218
11219 if ( true !== $result ) {
11220 $error = FS_Api::is_api_error_object( $result ) ?
11221 $result->error->message :
11222 var_export( $result, true );
11223 }
11224 }
11225
11226 if ( empty( $error ) && ! empty( $site_ids ) ) {
11227 $result = $this->activate_license_on_many_sites( $user, $license_key, $site_ids );
11228
11229 if ( true !== $result ) {
11230 $error = FS_Api::is_api_error_object( $result ) ?
11231 $result->error->message :
11232 var_export( $result, true );
11233 }
11234 }
11235 } else {
11236 if ( $has_valid_blog_id ) {
11237 /**
11238 * If a specific blog ID was provided, activate the license only for the install that is
11239 * associated with the given blog ID.
11240 *
11241 * @author Leo Fajardo (@leorw)
11242 */
11243 $this->switch_to_blog( $blog_id );
11244 }
11245
11246 $api = $fs->get_api_site_scope();
11247
11248 $params = array(
11249 'license_key' => $fs->apply_filters( 'license_key', $license_key )
11250 );
11251
11252 $install = $api->call( $fs->add_show_pending( '/' ), 'put', $params );
11253
11254 if ( FS_Api::is_api_error( $install ) ) {
11255 $error = FS_Api::is_api_error_object( $install ) ?
11256 $install->error->message :
11257 var_export( $install->error, true );
11258 } else {
11259 $fs->reconnect_locally( $has_valid_blog_id );
11260 }
11261 }
11262
11263 if ( empty( $error ) ) {
11264 $this->network_upgrade_mode_completed();
11265
11266 $fs->_sync_license( true, $has_valid_blog_id );
11267
11268 $next_page = $fs->is_addon() ?
11269 $fs->get_parent_instance()->get_account_url() :
11270 $fs->get_account_url();
11271 }
11272 } else {
11273 $next_page = $fs->opt_in(
11274 false,
11275 false,
11276 false,
11277 $license_key,
11278 false,
11279 false,
11280 false,
11281 fs_request_get_bool( 'is_marketing_allowed', null ),
11282 $sites
11283 );
11284
11285 if ( isset( $next_page->error ) ) {
11286 $error = $next_page->error;
11287 } else {
11288 if ( fs_is_network_admin() ) {
11289 /**
11290 * Get the list of sites that were just opted-in (and license activated).
11291 * This is an optimization for the next part below saving some DB queries.
11292 */
11293 $connected_sites = array();
11294 foreach ( $sites as $site ) {
11295 if ( isset( $site['blog_id'] ) && is_numeric( $site['blog_id'] ) ) {
11296 $connected_sites[ $site['blog_id'] ] = true;
11297 }
11298 }
11299
11300 $all_sites = self::get_sites();
11301 $pending_sites = array();
11302
11303 /**
11304 * 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.
11305 *
11306 * @author Vova Feldman (@svovaf)
11307 */
11308 foreach ( $all_sites as $site ) {
11309 $blog_id = self::get_site_blog_id( $site );
11310
11311 if ( isset( $connected_sites[ $blog_id ] ) ) {
11312 // Site was just connected.
11313 continue;
11314 }
11315
11316 if ( $this->is_installed_on_site( $blog_id ) ) {
11317 // Site was already connected before.
11318 continue;
11319 }
11320
11321 if ( $this->is_site_delegated_connection( $blog_id ) ) {
11322 // Site's connection was delegated.
11323 continue;
11324 }
11325
11326 if ( $this->is_anonymous_site( $blog_id ) ) {
11327 // Site connection was already skipped.
11328 continue;
11329 }
11330
11331 $pending_sites[] = self::get_site_info( $site );
11332 }
11333
11334 if ( ! empty( $pending_sites ) ) {
11335 if ( $this->is_freemium() ) {
11336 $this->skip_connection( $pending_sites );
11337 } else {
11338 $this->delegate_connection( $pending_sites );
11339 }
11340 }
11341 }
11342 }
11343 }
11344
11345 if ( false === $error && true === $this->_storage->require_license_activation ) {
11346 $this->_storage->require_license_activation = false;
11347 }
11348
11349 $result = array(
11350 'success' => ( false === $error )
11351 );
11352
11353 if ( false !== $error ) {
11354 $result['error'] = $error;
11355 } else {
11356 $result['next_page'] = $next_page;
11357 }
11358
11359 echo json_encode( $result );
11360
11361 exit;
11362 }
11363
11364 /**
11365 * @author Leo Fajardo (@leorw)
11366 * @since 1.2.3.1
11367 */
11368 function _network_activate_ajax_action() {
11369 $this->_logger->entrance();
11370
11371 $this->check_ajax_referer( 'network_activate' );
11372
11373 $plugin_id = fs_request_get( 'module_id', '', 'post' );
11374 $fs = ( $plugin_id == $this->_module_id ) ?
11375 $this :
11376 $this->get_addon_instance( $plugin_id );
11377
11378 $error = false;
11379
11380 $sites = fs_request_get( 'sites', array(), 'post' );
11381 if ( is_array( $sites ) && ! empty( $sites ) ) {
11382 $sites_by_action = array(
11383 'allow' => array(),
11384 'delegate' => array(),
11385 'skip' => array()
11386 );
11387
11388 foreach ( $sites as $site ) {
11389 $sites_by_action[ $site['action'] ][] = $site;
11390 }
11391
11392 $total_sites = count( $sites );
11393 $total_sites_to_delegate = count( $sites_by_action['delegate'] );
11394
11395 $next_page = '';
11396 if ( $total_sites === $total_sites_to_delegate &&
11397 ! $this->is_network_upgrade_mode()
11398 ) {
11399 $this->delegate_connection();
11400 } else {
11401 if ( ! empty( $sites_by_action['delegate'] ) ) {
11402 $this->delegate_connection( $sites_by_action['delegate'] );
11403 }
11404
11405 if ( ! empty( $sites_by_action['skip'] ) ) {
11406 $this->skip_connection( $sites_by_action['skip'] );
11407 }
11408
11409 if ( ! empty( $sites_by_action['allow'] ) ) {
11410 if ( ! $fs->is_registered() || ! $this->_is_network_active ) {
11411 $next_page = $fs->opt_in(
11412 false,
11413 false,
11414 false,
11415 false,
11416 false,
11417 false,
11418 false,
11419 fs_request_get_bool( 'is_marketing_allowed', null ),
11420 $sites_by_action['allow']
11421 );
11422 } else {
11423 $next_page = $fs->install_with_user(
11424 $this->get_network_user(),
11425 false,
11426 false,
11427 false,
11428 true,
11429 $sites_by_action['allow']
11430 );
11431 }
11432
11433 if ( is_object( $next_page ) && isset( $next_page->error ) ) {
11434 $error = $next_page->error;
11435 }
11436 }
11437 }
11438
11439 if ( empty( $next_page ) ) {
11440 $next_page = $this->get_after_activation_url( 'after_network_activation_url' );
11441 }
11442 } else {
11443 $error = $this->get_text_inline( 'Invalid site details collection.', 'invalid_site_details_collection' );
11444 }
11445
11446 $result = array(
11447 'success' => ( false === $error )
11448 );
11449
11450 if ( false !== $error ) {
11451 $result['error'] = $error;
11452 } else {
11453 $result['next_page'] = $next_page;
11454 }
11455
11456 echo json_encode( $result );
11457
11458 exit;
11459 }
11460
11461 /**
11462 * Billing update AJAX callback.
11463 *
11464 * @author Vova Feldman (@svovaf)
11465 * @since 1.2.1.5
11466 */
11467 function _update_billing_ajax_action() {
11468 $this->_logger->entrance();
11469
11470 $this->check_ajax_referer( 'update_billing' );
11471
11472 if ( ! $this->is_user_admin() ) {
11473 // Only for admins.
11474 self::shoot_ajax_failure();
11475 }
11476
11477 $billing = fs_request_get( 'billing' );
11478
11479 $api = $this->get_api_user_scope();
11480 $result = $api->call( '/billing.json', 'put', array_merge( $billing, array(
11481 'plugin_id' => $this->get_parent_id(),
11482 ) ) );
11483
11484 if ( ! $this->is_api_result_entity( $result ) ) {
11485 self::shoot_ajax_failure();
11486 }
11487
11488 // Purge cached billing.
11489 $this->get_api_user_scope()->purge_cache( 'billing.json' );
11490
11491 self::shoot_ajax_success();
11492 }
11493
11494 /**
11495 * Trial start for anonymous users (AJAX callback).
11496 *
11497 * @author Vova Feldman (@svovaf)
11498 * @since 1.2.1.5
11499 */
11500 function _start_trial_ajax_action() {
11501 $this->_logger->entrance();
11502
11503 $this->check_ajax_referer( 'start_trial' );
11504
11505 if ( ! $this->is_user_admin() ) {
11506 // Only for admins.
11507 self::shoot_ajax_failure();
11508 }
11509
11510 $trial_data = fs_request_get( 'trial' );
11511
11512 $next_page = $this->opt_in(
11513 false,
11514 false,
11515 false,
11516 false,
11517 false,
11518 $trial_data['plan_id']
11519 );
11520
11521 if ( is_object( $next_page ) && $this->is_api_error( $next_page ) ) {
11522 self::shoot_ajax_failure(
11523 isset( $next_page->error ) ?
11524 $next_page->error->message :
11525 var_export( $next_page, true )
11526 );
11527 }
11528
11529 $this->shoot_ajax_success( array(
11530 'next_page' => $next_page,
11531 ) );
11532 }
11533
11534 /**
11535 * @author Leo Fajardo (@leorw)
11536 * @since 1.2.0
11537 */
11538 function _resend_license_key_ajax_action() {
11539 $this->_logger->entrance();
11540
11541 $this->check_ajax_referer( 'resend_license_key' );
11542
11543 $email_address = sanitize_email( trim( fs_request_get( 'email', '', 'post' ) ) );
11544
11545 if ( empty( $email_address ) ) {
11546 exit;
11547 }
11548
11549 $error = false;
11550
11551 $api = $this->get_api_plugin_scope();
11552 $result = $api->call( '/licenses/resend.json', 'post',
11553 array(
11554 'email' => $email_address,
11555 'url' => home_url(),
11556 )
11557 );
11558
11559 if ( is_object( $result ) && isset( $result->error ) ) {
11560 $error = $result->error;
11561
11562 if ( in_array( $error->code, array( 'invalid_email', 'no_user' ) ) ) {
11563 $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' );
11564 } else if ( 'no_license' === $error->code ) {
11565 $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' );
11566 } else {
11567 $error = $error->message;
11568 }
11569 }
11570
11571 $licenses = array(
11572 'success' => ( false === $error )
11573 );
11574
11575 if ( false !== $error ) {
11576 $licenses['error'] = sprintf( '%s... %s', $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ), strtolower( $error ) );
11577 }
11578
11579 echo json_encode( $licenses );
11580
11581 exit;
11582 }
11583
11584 /**
11585 * @author Vova Feldman (@svovaf)
11586 * @since 1.2.1.8
11587 *
11588 * @var string
11589 */
11590 private static $_pagenow;
11591
11592 /**
11593 * Get current page or the referer if executing a WP AJAX request.
11594 *
11595 * @author Vova Feldman (@svovaf)
11596 * @since 1.2.1.8
11597 *
11598 * @return string
11599 */
11600 static function get_current_page() {
11601 if ( ! isset( self::$_pagenow ) ) {
11602 global $pagenow;
11603 if ( empty( $pagenow ) && is_admin() && is_multisite() ) {
11604 /**
11605 * It appears that `$pagenow` is not yet initialized in some network admin pages when this method
11606 * is called, so initialize it here using some pieces of code from `wp-includes/vars.php`.
11607 *
11608 * @author Leo Fajardo (@leorw)
11609 * @since 2.2.3
11610 */
11611 if ( is_network_admin() ) {
11612 preg_match( '#/wp-admin/network/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
11613 } else if ( is_user_admin() ) {
11614 preg_match( '#/wp-admin/user/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
11615 } else {
11616 preg_match( '#/wp-admin/?(.*?)$#i', $_SERVER['PHP_SELF'], $self_matches );
11617 }
11618
11619 $pagenow = $self_matches[1];
11620 $pagenow = trim( $pagenow, '/' );
11621 $pagenow = preg_replace( '#\?.*?$#', '', $pagenow );
11622 if ( '' === $pagenow || 'index' === $pagenow || 'index.php' === $pagenow ) {
11623 $pagenow = 'index.php';
11624 } else {
11625 preg_match( '#(.*?)(/|$)#', $pagenow, $self_matches );
11626 $pagenow = strtolower( $self_matches[1] );
11627 if ( '.php' !== substr($pagenow, -4, 4) )
11628 $pagenow .= '.php'; // for Options +Multiviews: /wp-admin/themes/index.php (themes.php is queried)
11629 }
11630 }
11631
11632 self::$_pagenow = $pagenow;
11633
11634 if ( self::is_ajax() &&
11635 'admin-ajax.php' === $pagenow
11636 ) {
11637 $referer = fs_get_raw_referer();
11638
11639 if ( is_string( $referer ) ) {
11640 $parts = explode( '?', $referer );
11641
11642 self::$_pagenow = basename( $parts[0] );
11643 }
11644 }
11645 }
11646
11647 return self::$_pagenow;
11648 }
11649
11650 /**
11651 * Helper method to check if user in the plugins page.
11652 *
11653 * @author Vova Feldman (@svovaf)
11654 * @since 1.2.1.5
11655 *
11656 * @return bool
11657 */
11658 static function is_plugins_page() {
11659 return ( 'plugins.php' === self::get_current_page() );
11660 }
11661
11662 /**
11663 * @author Leo Fajardo (@leorw)
11664 * @since 2.2.3
11665 *
11666 * @return bool
11667 */
11668 static function is_plugin_install_page() {
11669 return ( 'plugin-install.php' === self::get_current_page() );
11670 }
11671
11672 /**
11673 * @author Leo Fajardo (@leorw)
11674 * @since 2.0.2
11675 *
11676 * @return bool
11677 */
11678 static function is_updates_page() {
11679 return ( 'update-core.php' === self::get_current_page() );
11680 }
11681
11682 /**
11683 * Helper method to check if user in the themes page.
11684 *
11685 * @author Vova Feldman (@svovaf)
11686 * @since 1.2.2.6
11687 *
11688 * @return bool
11689 */
11690 static function is_themes_page() {
11691 return ( 'themes.php' === self::get_current_page() );
11692 }
11693
11694 #----------------------------------------------------------------------------------
11695 #region Affiliation
11696 #----------------------------------------------------------------------------------
11697
11698 /**
11699 * @author Leo Fajardo (@leorw)
11700 * @since 1.2.3
11701 *
11702 * @return bool
11703 */
11704 function has_affiliate_program() {
11705 if ( ! is_object( $this->_plugin ) ) {
11706 return false;
11707 }
11708
11709 return $this->_plugin->has_affiliate_program();
11710 }
11711
11712 /**
11713 * @author Leo Fajardo (@leorw)
11714 * @since 1.2.4
11715 */
11716 private function fetch_affiliate_terms() {
11717 if ( ! is_object( $this->plugin_affiliate_terms ) ) {
11718 $plugins_api = $this->get_api_plugin_scope();
11719 $affiliate_terms = $plugins_api->get( '/aff.json?type=affiliation', false );
11720
11721 if ( ! $this->is_api_result_entity( $affiliate_terms ) ) {
11722 return;
11723 }
11724
11725 $this->plugin_affiliate_terms = new FS_AffiliateTerms( $affiliate_terms );
11726 }
11727 }
11728
11729 /**
11730 * @author Leo Fajardo (@leorw)
11731 * @since 1.2.4
11732 */
11733 private function fetch_affiliate_and_custom_terms() {
11734 if ( ! empty( $this->_storage->affiliate_application_data ) ) {
11735 $application_data = $this->_storage->affiliate_application_data;
11736 $flush = ( ! isset( $application_data['status'] ) || 'pending' === $application_data['status'] );
11737
11738 $users_api = $this->get_api_user_scope();
11739 $result = $users_api->get( "/plugins/{$this->_plugin->id}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json", $flush );
11740 if ( $this->is_api_result_object( $result, 'affiliates' ) ) {
11741 if ( ! empty( $result->affiliates ) ) {
11742 $affiliate = new FS_Affiliate( $result->affiliates[0] );
11743
11744 if ( ! isset( $application_data['status'] ) || $application_data['status'] !== $affiliate->status ) {
11745 $application_data['status'] = $affiliate->status;
11746 $this->_storage->affiliate_application_data = $application_data;
11747 }
11748
11749 if ( $affiliate->is_using_custom_terms ) {
11750 $affiliate_terms = $users_api->get( "/plugins/{$this->_plugin->id}/affiliates/{$affiliate->id}/aff/{$affiliate->custom_affiliate_terms_id}.json", $flush );
11751 if ( $this->is_api_result_entity( $affiliate_terms ) ) {
11752 $this->custom_affiliate_terms = new FS_AffiliateTerms( $affiliate_terms );
11753 }
11754 }
11755
11756 $this->affiliate = $affiliate;
11757 }
11758 }
11759 }
11760 }
11761
11762 /**
11763 * @author Leo Fajardo (@leorw)
11764 * @since 1.2.3
11765 */
11766 private function fetch_affiliate_and_terms() {
11767 $this->_logger->entrance();
11768
11769 $this->fetch_affiliate_terms();
11770 $this->fetch_affiliate_and_custom_terms();
11771 }
11772
11773 /**
11774 * @author Leo Fajardo (@leorw)
11775 * @since 1.2.3
11776 *
11777 * @return FS_Affiliate
11778 */
11779 function get_affiliate() {
11780 return $this->affiliate;
11781 }
11782
11783
11784 /**
11785 * @author Leo Fajardo (@leorw)
11786 * @since 1.2.3
11787 *
11788 * @return FS_AffiliateTerms
11789 */
11790 function get_affiliate_terms() {
11791 return is_object( $this->custom_affiliate_terms ) ?
11792 $this->custom_affiliate_terms :
11793 $this->plugin_affiliate_terms;
11794 }
11795
11796 /**
11797 * @author Leo Fajardo (@leorw)
11798 * @since 1.2.3
11799 */
11800 function _submit_affiliate_application() {
11801 $this->_logger->entrance();
11802
11803 $this->check_ajax_referer( 'submit_affiliate_application' );
11804
11805 if ( ! $this->is_user_admin() ) {
11806 // Only for admins.
11807 self::shoot_ajax_failure();
11808 }
11809
11810 $affiliate = fs_request_get( 'affiliate' );
11811
11812 if ( empty( $affiliate['promotion_methods'] ) ) {
11813 unset( $affiliate['promotion_methods'] );
11814 }
11815
11816 if ( ! empty( $affiliate['additional_domains'] ) ) {
11817 $affiliate['additional_domains'] = array_unique( $affiliate['additional_domains'] );
11818 }
11819
11820 if ( ! $this->is_registered() ) {
11821 // Opt in but don't track usage.
11822 $next_page = $this->opt_in(
11823 false,
11824 false,
11825 false,
11826 false,
11827 false,
11828 false,
11829 true
11830 );
11831
11832 if ( is_object( $next_page ) && $this->is_api_error( $next_page ) ) {
11833 self::shoot_ajax_failure(
11834 isset( $next_page->error ) ?
11835 $next_page->error->message :
11836 var_export( $next_page, true )
11837 );
11838 } else if ( $this->is_pending_activation() ) {
11839 self::shoot_ajax_failure( $this->get_text_inline( 'Account is pending activation.', 'account-is-pending-activation' ) );
11840 }
11841 }
11842
11843 $this->fetch_affiliate_terms();
11844
11845 $api = $this->get_api_user_scope();
11846 $result = $api->call(
11847 ( "/plugins/{$this->_plugin->id}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json" ),
11848 'post',
11849 $affiliate
11850 );
11851
11852 if ( $this->is_api_error( $result ) ) {
11853 self::shoot_ajax_failure(
11854 isset( $result->error ) ?
11855 $result->error->message :
11856 var_export( $result, true )
11857 );
11858 } else {
11859 if ( $this->_admin_notices->has_sticky( 'affiliate_program' ) ) {
11860 $this->_admin_notices->remove_sticky( 'affiliate_program' );
11861 }
11862
11863 $affiliate_application_data = array(
11864 'status' => 'pending',
11865 'stats_description' => $affiliate['stats_description'],
11866 'promotion_method_description' => $affiliate['promotion_method_description'],
11867 );
11868
11869 if ( ! empty( $affiliate['promotion_methods'] ) ) {
11870 $affiliate_application_data['promotion_methods'] = $affiliate['promotion_methods'];
11871 }
11872
11873 if ( ! empty( $affiliate['domain'] ) ) {
11874 $affiliate_application_data['domain'] = $affiliate['domain'];
11875 }
11876
11877 if ( ! empty( $affiliate['additional_domains'] ) ) {
11878 $affiliate_application_data['additional_domains'] = $affiliate['additional_domains'];
11879 }
11880
11881 $this->_storage->affiliate_application_data = $affiliate_application_data;
11882 }
11883
11884 // Purge cached affiliate.
11885 $api->purge_cache( 'affiliate.json' );
11886
11887 self::shoot_ajax_success( $result );
11888 }
11889
11890 /**
11891 * @author Leo Fajardo (@leorw)
11892 * @since 1.2.3
11893 *
11894 * @return array|null
11895 */
11896 function get_affiliate_application_data() {
11897 if ( empty( $this->_storage->affiliate_application_data ) ) {
11898 return null;
11899 }
11900
11901 return $this->_storage->affiliate_application_data;
11902 }
11903
11904 #endregion Affiliation ------------------------------------------------------------
11905
11906 #----------------------------------------------------------------------------------
11907 #region URL Generators
11908 #----------------------------------------------------------------------------------
11909
11910 /**
11911 * Alias to pricing_url().
11912 *
11913 * @author Vova Feldman (@svovaf)
11914 * @since 1.0.2
11915 *
11916 * @uses pricing_url()
11917 *
11918 * @param string $period Billing cycle
11919 * @param bool $is_trial
11920 *
11921 * @return string
11922 */
11923 function get_upgrade_url( $period = WP_FS__PERIOD_ANNUALLY, $is_trial = false ) {
11924 return $this->pricing_url( $period, $is_trial );
11925 }
11926
11927 /**
11928 * @author Vova Feldman (@svovaf)
11929 * @since 1.0.9
11930 *
11931 * @uses get_upgrade_url()
11932 *
11933 * @return string
11934 */
11935 function get_trial_url() {
11936 return $this->get_upgrade_url( WP_FS__PERIOD_ANNUALLY, true );
11937 }
11938
11939 /**
11940 * @author Leo Fajardo (@leorw)
11941 * @since 2.1.4
11942 *
11943 * @param string $new_version
11944 *
11945 * @return string
11946 */
11947 function version_upgrade_checkout_link( $new_version ) {
11948 if ( ! is_object( $this->_license ) ) {
11949 $url = $this->pricing_url();
11950
11951 $purchase_license_text = $this->get_text_inline( 'Buy a license now', 'buy-license-now' );
11952 } else {
11953 $subscription = $this->_get_subscription( $this->_license->id );
11954
11955 $url = $this->checkout_url(
11956 is_object( $subscription ) ?
11957 ( 1 == $subscription->billing_cycle ? WP_FS__PERIOD_MONTHLY : WP_FS__PERIOD_ANNUALLY ) :
11958 WP_FS__PERIOD_LIFETIME,
11959 false,
11960 array( 'licenses' => $this->_license->quota )
11961 );
11962
11963 $purchase_license_text = $this->get_text_inline( 'Renew your license now', 'renew-license-now' );
11964 }
11965
11966 return sprintf(
11967 $this->get_text_inline( '%s to access version %s security & feature updates, and support.', 'x-for-updates-and-support' ),
11968 sprintf( '<a href="%s">%s</a>', $url, $purchase_license_text ),
11969 $new_version
11970 );
11971 }
11972
11973 /**
11974 * Plugin's pricing URL.
11975 *
11976 * @author Vova Feldman (@svovaf)
11977 * @since 1.0.4
11978 *
11979 * @param string $billing_cycle Billing cycle
11980 *
11981 * @param bool $is_trial
11982 *
11983 * @return string
11984 */
11985 function pricing_url( $billing_cycle = WP_FS__PERIOD_ANNUALLY, $is_trial = false ) {
11986 $this->_logger->entrance();
11987
11988 $params = array(
11989 'billing_cycle' => $billing_cycle
11990 );
11991
11992 if ( $is_trial ) {
11993 $params['trial'] = 'true';
11994 }
11995
11996 if ( $this->is_addon() ) {
11997 return $this->_parent->addon_url( $this->_slug );
11998 }
11999
12000 return $this->_get_admin_page_url( 'pricing', $params );
12001 }
12002
12003 /**
12004 * Checkout page URL.
12005 *
12006 * @author Vova Feldman (@svovaf)
12007 * @since 1.0.6
12008 *
12009 * @param string $billing_cycle Billing cycle
12010 * @param bool $is_trial
12011 * @param array $extra (optional) Extra parameters, override other query params.
12012 *
12013 * @return string
12014 */
12015 function checkout_url(
12016 $billing_cycle = WP_FS__PERIOD_ANNUALLY,
12017 $is_trial = false,
12018 $extra = array()
12019 ) {
12020 $this->_logger->entrance();
12021
12022 $params = array(
12023 'checkout' => 'true',
12024 'billing_cycle' => $billing_cycle,
12025 );
12026
12027 if ( $is_trial ) {
12028 $params['trial'] = 'true';
12029 }
12030
12031 /**
12032 * Params in extra override other params.
12033 */
12034 $params = array_merge( $params, $extra );
12035
12036 return $this->_get_admin_page_url( 'pricing', $params );
12037 }
12038
12039 /**
12040 * Add-on checkout URL.
12041 *
12042 * @author Vova Feldman (@svovaf)
12043 * @since 1.1.7
12044 *
12045 * @param number $addon_id
12046 * @param number $pricing_id
12047 * @param string $billing_cycle
12048 * @param bool $is_trial
12049 *
12050 * @return string
12051 */
12052 function addon_checkout_url(
12053 $addon_id,
12054 $pricing_id,
12055 $billing_cycle = WP_FS__PERIOD_ANNUALLY,
12056 $is_trial = false
12057 ) {
12058 return $this->checkout_url( $billing_cycle, $is_trial, array(
12059 'plugin_id' => $addon_id,
12060 'pricing_id' => $pricing_id,
12061 ) );
12062 }
12063
12064 #endregion
12065
12066 #endregion ------------------------------------------------------------------
12067
12068 /**
12069 * Check if plugin has any add-ons.
12070 *
12071 * @author Vova Feldman (@svovaf)
12072 * @since 1.0.5
12073 *
12074 * @since 1.1.7.3 Base logic only on the parameter provided by the developer in the init function.
12075 *
12076 * @return bool
12077 */
12078 function has_addons() {
12079 $this->_logger->entrance();
12080
12081 return $this->_has_addons;
12082 }
12083
12084 /**
12085 * Check if plugin can work in anonymous mode.
12086 *
12087 * @author Vova Feldman (@svovaf)
12088 * @since 1.0.9
12089 *
12090 * @return bool
12091 *
12092 * @deprecated Please use is_enable_anonymous() instead.
12093 */
12094 function enable_anonymous() {
12095 return $this->_enable_anonymous;
12096 }
12097
12098 /**
12099 * Check if plugin can work in anonymous mode.
12100 *
12101 * @author Vova Feldman (@svovaf)
12102 * @since 1.1.9
12103 *
12104 * @return bool
12105 */
12106 function is_enable_anonymous() {
12107 return $this->_enable_anonymous;
12108 }
12109
12110 /**
12111 * Check if plugin is premium only (no free plans).
12112 *
12113 * @author Vova Feldman (@svovaf)
12114 * @since 1.1.9
12115 *
12116 * @return bool
12117 */
12118 function is_only_premium() {
12119 return $this->_is_premium_only;
12120 }
12121
12122 /**
12123 * Checks if the plugin's type is "plugin". The other type is "theme".
12124 *
12125 * @author Leo Fajardo (@leorw)
12126 * @since 1.2.2
12127 *
12128 * @return bool
12129 */
12130 function is_plugin() {
12131 return ( WP_FS__MODULE_TYPE_PLUGIN === $this->_module_type );
12132 }
12133
12134 /**
12135 * @author Leo Fajardo (@leorw)
12136 * @since 1.2.2
12137 *
12138 * @return string
12139 */
12140 function get_module_type() {
12141 if ( ! isset( $this->_module_type ) ) {
12142 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
12143 $this->_module_type = $id_slug_type_path_map[ $this->_module_id ]['type'];
12144 }
12145
12146 return $this->_module_type;
12147 }
12148
12149 /**
12150 * @author Leo Fajardo (@leorw)
12151 * @since 1.2.2
12152 *
12153 * @return string
12154 */
12155 function get_plugin_main_file_path() {
12156 return $this->_plugin_main_file_path;
12157 }
12158
12159 /**
12160 * Check if module has a premium code version.
12161 *
12162 * Serviceware module might be freemium without any
12163 * premium code version, where the paid features
12164 * are all part of the service.
12165 *
12166 * @author Vova Feldman (@svovaf)
12167 * @since 1.2.1.6
12168 *
12169 * @return bool
12170 */
12171 function has_premium_version() {
12172 return $this->_has_premium_version;
12173 }
12174
12175 /**
12176 * Check if feature supported with current site's plan.
12177 *
12178 * @author Vova Feldman (@svovaf)
12179 * @since 1.0.1
12180 *
12181 * @todo IMPLEMENT
12182 *
12183 * @param number $feature_id
12184 *
12185 * @throws Exception
12186 */
12187 function is_feature_supported( $feature_id ) {
12188 throw new Exception( 'not implemented' );
12189 }
12190
12191 /**
12192 * @author Vova Feldman (@svovaf)
12193 * @since 1.0.1
12194 *
12195 * @return bool Is running in SSL/HTTPS
12196 */
12197 function is_ssl() {
12198 return WP_FS__IS_HTTPS;
12199 }
12200
12201 /**
12202 * @author Vova Feldman (@svovaf)
12203 * @since 1.0.9
12204 *
12205 * @return bool Is running in AJAX call.
12206 *
12207 * @link http://wordpress.stackexchange.com/questions/70676/how-to-check-if-i-am-in-admin-ajax
12208 */
12209 static function is_ajax() {
12210 return ( defined( 'DOING_AJAX' ) && DOING_AJAX );
12211 }
12212
12213 /**
12214 * Check if it's an AJAX call targeted for the current module.
12215 *
12216 * @author Vova Feldman (@svovaf)
12217 * @since 1.2.0
12218 *
12219 * @param array|string $actions Collection of AJAX actions.
12220 *
12221 * @return bool
12222 */
12223 function is_ajax_action( $actions ) {
12224 // Verify it's an ajax call.
12225 if ( ! self::is_ajax() ) {
12226 return false;
12227 }
12228
12229 // Verify the call is relevant for the plugin.
12230 if ( $this->_module_id != fs_request_get( 'module_id' ) ) {
12231 return false;
12232 }
12233
12234 // Verify it's one of the specified actions.
12235 if ( is_string( $actions ) ) {
12236 $actions = explode( ',', $actions );
12237 }
12238
12239 if ( is_array( $actions ) && 0 < count( $actions ) ) {
12240 $ajax_action = fs_request_get( 'action' );
12241
12242 foreach ( $actions as $action ) {
12243 if ( $ajax_action === $this->get_action_tag( $action ) ) {
12244 return true;
12245 }
12246 }
12247 }
12248
12249 return false;
12250 }
12251
12252 /**
12253 * Check if it's an AJAX call targeted for current request.
12254 *
12255 * @author Vova Feldman (@svovaf)
12256 * @since 1.2.0
12257 *
12258 * @param array|string $actions Collection of AJAX actions.
12259 * @param number|null $module_id
12260 *
12261 * @return bool
12262 */
12263 static function is_ajax_action_static( $actions, $module_id = null ) {
12264 // Verify it's an ajax call.
12265 if ( ! self::is_ajax() ) {
12266 return false;
12267 }
12268
12269
12270 if ( ! empty( $module_id ) ) {
12271 // Verify the call is relevant for the plugin.
12272 if ( $module_id != fs_request_get( 'module_id' ) ) {
12273 return false;
12274 }
12275 }
12276
12277 // Verify it's one of the specified actions.
12278 if ( is_string( $actions ) ) {
12279 $actions = explode( ',', $actions );
12280 }
12281
12282 if ( is_array( $actions ) && 0 < count( $actions ) ) {
12283 $ajax_action = fs_request_get( 'action' );
12284
12285 foreach ( $actions as $action ) {
12286 if ( $ajax_action === self::get_ajax_action_static( $action, $module_id ) ) {
12287 return true;
12288 }
12289 }
12290 }
12291
12292 return false;
12293 }
12294
12295 /**
12296 * @author Vova Feldman (@svovaf)
12297 * @since 1.1.7
12298 *
12299 * @return bool
12300 */
12301 static function is_cron() {
12302 return ( defined( 'DOING_CRON' ) && DOING_CRON );
12303 }
12304
12305 /**
12306 * Check if a real user is visiting the admin dashboard.
12307 *
12308 * @author Vova Feldman (@svovaf)
12309 * @since 1.1.7
12310 *
12311 * @return bool
12312 */
12313 function is_user_in_admin() {
12314 return is_admin() && ! self::is_ajax() && ! self::is_cron();
12315 }
12316
12317 /**
12318 * Check if a real user is in the customizer view.
12319 *
12320 * @author Vova Feldman (@svovaf)
12321 * @since 1.2.2.7
12322 *
12323 * @return bool
12324 */
12325 static function is_customizer() {
12326 return is_customize_preview();
12327 }
12328
12329 /**
12330 * Check if running in HTTPS and if site's plan matching the specified plan.
12331 *
12332 * @param string $plan
12333 * @param bool $exact
12334 *
12335 * @return bool
12336 */
12337 function is_ssl_and_plan( $plan, $exact = false ) {
12338 return ( $this->is_ssl() && $this->is_plan( $plan, $exact ) );
12339 }
12340
12341 /**
12342 * Construct plugin's settings page URL.
12343 *
12344 * @author Vova Feldman (@svovaf)
12345 * @since 1.0.4
12346 *
12347 * @param string $page
12348 * @param array $params
12349 * @param bool|null $network
12350 *
12351 * @return string
12352 */
12353 function _get_admin_page_url( $page = '', $params = array(), $network = null ) {
12354 if ( is_null( $network ) ) {
12355 $network = (
12356 $this->_is_network_active &&
12357 ( fs_is_network_admin() || ! $this->is_delegated_connection() )
12358 );
12359 }
12360
12361 if ( 0 < count( $params ) ) {
12362 foreach ( $params as $k => $v ) {
12363 $params[ $k ] = urlencode( $v );
12364 }
12365 }
12366
12367 $page_param = $this->_menu->get_slug( $page );
12368
12369 if ( empty( $page ) &&
12370 $this->is_theme() &&
12371 // Show the opt-in as an overlay for free wp.org themes or themes without any settings page.
12372 ( $this->is_free_wp_org_theme() || ! $this->has_settings_menu() )
12373 ) {
12374 $params[ $this->get_unique_affix() . '_show_optin' ] = 'true';
12375
12376 return add_query_arg(
12377 $params,
12378 $this->admin_url( 'themes.php', 'admin', $network )
12379 );
12380 }
12381
12382 if ( ! $this->has_settings_menu() ) {
12383 if ( ! empty( $page ) ) {
12384 // Module doesn't have a setting page, but since the request is for
12385 // a specific Freemius page, use the admin.php path.
12386 return add_query_arg( array_merge( $params, array(
12387 'page' => $page_param,
12388 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
12389 } else {
12390 if ( $this->is_activation_mode() ) {
12391 /**
12392 * @author Vova Feldman
12393 * @since 1.2.1.6
12394 *
12395 * If plugin doesn't have a settings page, create one for the opt-in screen.
12396 */
12397 return add_query_arg( array_merge( $params, array(
12398 'page' => $this->_slug,
12399 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
12400 } else {
12401 // Plugin without a settings page.
12402 return add_query_arg(
12403 $params,
12404 $this->admin_url( 'plugins.php', 'admin', $network )
12405 );
12406 }
12407 }
12408 }
12409
12410 // Module has a submenu settings page.
12411 if ( ! $this->_menu->is_top_level() ) {
12412 $parent_slug = $this->_menu->get_parent_slug();
12413 $menu_file = ( false !== strpos( $parent_slug, '.php' ) ) ?
12414 $parent_slug :
12415 'admin.php';
12416
12417 return add_query_arg( array_merge( $params, array(
12418 'page' => $page_param,
12419 ) ), $this->admin_url( $menu_file, 'admin', $network ) );
12420 }
12421
12422 // Module has a top level CPT settings page.
12423 if ( $this->_menu->is_cpt() ) {
12424 if ( empty( $page ) && $this->is_activation_mode() ) {
12425 return add_query_arg( array_merge( $params, array(
12426 'page' => $page_param
12427 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
12428 } else {
12429 if ( ! empty( $page ) ) {
12430 $params['page'] = $page_param;
12431 }
12432
12433 return add_query_arg(
12434 $params,
12435 $this->admin_url( $this->_menu->get_raw_slug(), 'admin', $network )
12436 );
12437 }
12438 }
12439
12440 // Module has a custom top level settings page.
12441 return add_query_arg( array_merge( $params, array(
12442 'page' => $page_param,
12443 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
12444 }
12445
12446 #--------------------------------------------------------------------------------
12447 #region Multisite
12448 #--------------------------------------------------------------------------------
12449
12450 /**
12451 * @author Leo Fajardo (@leorw)
12452 * @since 2.0.0
12453 *
12454 * @return bool
12455 */
12456 function is_network_active() {
12457 return $this->_is_network_active;
12458 }
12459
12460 /**
12461 * Delegate activation for the given sites in the network (or all sites if `null`) to site admins.
12462 *
12463 * @author Leo Fajardo (@leorw)
12464 * @since 2.0.0
12465 *
12466 * @param array|null $sites
12467 */
12468 private function delegate_connection( $sites = null ) {
12469 $this->_logger->entrance();
12470
12471 $this->_admin_notices->remove_sticky( 'connect_account' );
12472
12473 if ( is_null( $sites ) ) {
12474 // All sites delegation.
12475 $this->_storage->store( 'is_delegated_connection', true, true, true );
12476 } else {
12477 // Specified sites delegation.
12478 foreach ( $sites as $site ) {
12479 $this->delegate_site_connection( $site['blog_id'] );
12480 }
12481 }
12482
12483 $this->network_upgrade_mode_completed();
12484 }
12485
12486 /**
12487 * Delegate specific network site conncetion to the site admin.
12488 *
12489 * @author Vova Feldman (@svovaf)
12490 * @since 2.0.0
12491 *
12492 * @param int $blog_id
12493 */
12494 private function delegate_site_connection( $blog_id ) {
12495 $this->_storage->store( 'is_delegated_connection', true, $blog_id, true );
12496 }
12497
12498 /**
12499 * Check if super-admin delegated the connection of ALL sites to the site admins.
12500 *
12501 * @author Vova Feldman (@svovaf)
12502 * @since 2.0.0
12503 *
12504 * @return bool
12505 */
12506 function is_network_delegated_connection() {
12507 if ( ! $this->_is_network_active ) {
12508 return false;
12509 }
12510
12511 return $this->_storage->get( 'is_delegated_connection', false, true );
12512 }
12513
12514 /**
12515 * @author Leo Fajardo (@leorw)
12516 * @since 2.0.0
12517 *
12518 * @param int $blog_id
12519 *
12520 * @return bool
12521 */
12522 function is_site_delegated_connection( $blog_id = 0 ) {
12523 if ( ! $this->_is_network_active ) {
12524 return false;
12525 }
12526
12527 if ( 0 == $blog_id ) {
12528 $blog_id = get_current_blog_id();
12529 }
12530
12531 return $this->_storage->get( 'is_delegated_connection', false, $blog_id );
12532 }
12533
12534 /**
12535 * Check if delegated the connection. When running within the the network admin,
12536 * and haven't specified the blog ID, checks if network level delegated. If running
12537 * within a site admin or specified a blog ID, check if delegated the connection for
12538 * the current context site.
12539 *
12540 * If executed outside the the admin, check if delegated the connection
12541 * for the current context site OR the whole network.
12542 *
12543 * @author Vova Feldman (@svovaf)
12544 * @since 2.0.0
12545 *
12546 * @param int $blog_id If set, checks if network delegated or blog specific delegated.
12547 *
12548 * @return bool
12549 */
12550 function is_delegated_connection( $blog_id = 0 ) {
12551 if ( ! $this->_is_network_active ) {
12552 return false;
12553 }
12554
12555 if ( fs_is_network_admin() && 0 == $blog_id ) {
12556 return $this->is_network_delegated_connection();
12557 }
12558
12559 return (
12560 $this->is_network_delegated_connection() ||
12561 $this->is_site_delegated_connection( $blog_id )
12562 );
12563 }
12564
12565 /**
12566 * Check if the current module is active for the site.
12567 *
12568 * @author Vova Feldman (@svovaf)
12569 * @since 2.0.0
12570 *
12571 * @param int $blog_id
12572 *
12573 * @return bool
12574 */
12575 function is_active_for_site( $blog_id ) {
12576 if ( ! is_multisite() ) {
12577 // Not a multisite and this code is executed, means that the plugin is active.
12578 return true;
12579 }
12580
12581 if ( $this->is_theme() ) {
12582 // All themes are site level activated.
12583 return true;
12584 }
12585
12586 if ( $this->_is_network_active ) {
12587 // Plugin was network activated so it's active.
12588 return true;
12589 }
12590
12591 return in_array( $this->_plugin_basename, (array) get_blog_option( $blog_id, 'active_plugins', array() ) );
12592 }
12593
12594 /**
12595 * @author Leo Fajardo (@leorw)
12596 * @since 2.0.0
12597 *
12598 * @return array Active & public sites collection.
12599 */
12600 static function get_sites() {
12601 /**
12602 * For consistency with get_blog_list() which only return active public sites.
12603 *
12604 * @author Vova Feldman (@svovaf)
12605 */
12606 $args = array(
12607 /**
12608 * Commented out in order to handle the migration of site options whether the site is public or not.
12609 *
12610 * @author Leo Fajardo (@leorw)
12611 * @since 2.2.1
12612 */
12613 // 'public' => 1,
12614 'archived' => 0,
12615 'mature' => 0,
12616 'spam' => 0,
12617 'deleted' => 0,
12618 );
12619
12620 if ( function_exists( 'get_sites' ) ) {
12621 // For WP 4.6 and above.
12622 return get_sites( $args );
12623 } else if ( function_exists( 'wp_get_sites' ) ) {
12624 // For WP 3.7 to WP 4.5.
12625 return wp_get_sites( $args );
12626 } else {
12627 // For WP 3.6 and below.
12628 return get_blog_list( 0, 'all' );
12629 }
12630 }
12631
12632 /**
12633 * Checks if a given blog is active.
12634 *
12635 * @author Vova Feldman (@svovaf)
12636 * @since 2.0.0
12637 *
12638 * @param $blog_id
12639 *
12640 * @return bool
12641 */
12642 private static function is_site_active( $blog_id ) {
12643 global $wpdb;
12644
12645 $blog_info = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->blogs} WHERE blog_id = %d", $blog_id ) );
12646
12647 if ( ! is_object( $blog_info ) ) {
12648 return false;
12649 }
12650
12651 return (
12652 true == $blog_info->public &&
12653 false == $blog_info->archived &&
12654 false == $blog_info->mature &&
12655 false == $blog_info->spam &&
12656 false == $blog_info->deleted
12657 );
12658 }
12659
12660 /**
12661 * Get a mapping between the site addresses to their blog IDs.
12662 *
12663 * @author Vova Feldman (@svovaf)
12664 * @since 2.0.0
12665 *
12666 * @return array {
12667 * @key string Site address without protocol with a trailing slash.
12668 * @value int Site's blog ID.
12669 * }
12670 */
12671 private function get_address_to_blog_map() {
12672 $sites = self::get_sites();
12673
12674 // Map site addresses to their blog IDs.
12675 $address_to_blog_map = array();
12676 foreach ( $sites as $site ) {
12677 $blog_id = self::get_site_blog_id( $site );
12678 $address = trailingslashit( fs_strip_url_protocol( get_site_url( $blog_id ) ) );
12679 $address_to_blog_map[ $address ] = $blog_id;
12680 }
12681
12682 return $address_to_blog_map;
12683 }
12684
12685 /**
12686 * Get a mapping between the site addresses to their blog IDs.
12687 *
12688 * @author Vova Feldman (@svovaf)
12689 * @since 2.0.0
12690 *
12691 * @return array {
12692 * @key int Site's blog ID.
12693 * @value FS_Site Associated install.
12694 * }
12695 */
12696 function get_blog_install_map() {
12697 $sites = self::get_sites();
12698
12699 // Map site blog ID to its install.
12700 $install_map = array();
12701
12702 foreach ( $sites as $site ) {
12703 $blog_id = self::get_site_blog_id( $site );
12704 $install = $this->get_install_by_blog_id( $blog_id );
12705
12706 if ( is_object( $install ) ) {
12707 $install_map[ $blog_id ] = $install;
12708 }
12709 }
12710
12711 return $install_map;
12712 }
12713
12714 /**
12715 * Gets a map of module IDs that the given user has opted-in to.
12716 *
12717 * @author Leo Fajardo (@leorw)
12718 * @since 2.1.0
12719 *
12720 * @param number $fs_user_id
12721 *
12722 * @return array {
12723 * @key number $plugin_id
12724 * @value bool Always true.
12725 * }
12726 */
12727 private static function get_user_opted_in_module_ids_map( $fs_user_id ) {
12728 self::$_static_logger->entrance();
12729
12730 if ( ! is_multisite() ) {
12731 $installs = array_merge(
12732 self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN ),
12733 self::get_all_sites( WP_FS__MODULE_TYPE_THEME )
12734 );
12735 } else {
12736 $sites = self::get_sites();
12737
12738 $installs = array();
12739 foreach ( $sites as $site ) {
12740 $blog_id = self::get_site_blog_id( $site );
12741
12742 $installs = array_merge(
12743 $installs,
12744 self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN, $blog_id ),
12745 self::get_all_sites( WP_FS__MODULE_TYPE_THEME, $blog_id )
12746 );
12747 }
12748 }
12749
12750 $module_ids_map = array();
12751 foreach ( $installs as $install ) {
12752 if ( is_object( $install ) &&
12753 FS_Site::is_valid_id( $install->id ) &&
12754 FS_User::is_valid_id( $install->user_id ) &&
12755 ( $install->user_id == $fs_user_id )
12756 ) {
12757 $module_ids_map[ $install->plugin_id ] = true;
12758 }
12759 }
12760
12761 return $module_ids_map;
12762 }
12763
12764 /**
12765 * @author Leo Fajardo (@leorw)
12766 *
12767 * @return null|array {
12768 * 'install' => FS_Site Module's install,
12769 * 'blog_id' => string The associated blog ID.
12770 * }
12771 */
12772 private function find_first_install() {
12773 $sites = self::get_sites();
12774
12775 foreach ( $sites as $site ) {
12776 $blog_id = self::get_site_blog_id( $site );
12777 $install = $this->get_install_by_blog_id( $blog_id );
12778
12779 if ( is_object( $install ) ) {
12780 return array(
12781 'install' => $install,
12782 'blog_id' => $blog_id
12783 );
12784 }
12785 }
12786
12787 return null;
12788 }
12789
12790 /**
12791 * Switches the Freemius site level context to a specified blog.
12792 *
12793 * @author Vova Feldman (@svovaf)
12794 * @since 2.0.0
12795 *
12796 * @param int $blog_id
12797 * @param FS_Site $install
12798 */
12799 function switch_to_blog( $blog_id, FS_Site $install = null ) {
12800 if ( $blog_id == $this->_context_is_network_or_blog_id ) {
12801 return;
12802 }
12803
12804 switch_to_blog( $blog_id );
12805 $this->_context_is_network_or_blog_id = $blog_id;
12806
12807 self::$_accounts->set_site_blog_context( $blog_id );
12808 $this->_storage->set_site_blog_context( $blog_id );
12809 $this->_storage->set_network_active( $this->_is_network_active, $this->is_delegated_connection( $blog_id ) );
12810
12811 $this->_site = is_object( $install ) ?
12812 $install :
12813 $this->get_install_by_blog_id( $blog_id );
12814
12815 $this->_user = false;
12816 $this->_licenses = false;
12817 $this->_license = null;
12818
12819 if ( is_object( $this->_site ) ) {
12820 // Try to fetch user from install.
12821 $this->_user = self::_get_user_by_id( $this->_site->user_id );
12822
12823 if ( ! is_object( $this->_user ) &&
12824 FS_User::is_valid_id( $this->_storage->prev_user_id )
12825 ) {
12826 // Try to fetch previously saved user.
12827 $this->_user = self::_get_user_by_id( $this->_storage->prev_user_id );
12828
12829 if ( ! is_object( $this->_user ) ) {
12830 // Fallback to network's user.
12831 $this->_user = $this->get_network_user();
12832 }
12833 }
12834
12835 $all_plugin_licenses = self::get_all_licenses( $this->_module_id );
12836
12837 if ( ! empty( $all_plugin_licenses ) ) {
12838 if ( ! FS_Plugin_License::is_valid_id( $this->_site->license_id ) ) {
12839 $this->_license = null;
12840 } else {
12841 $license_found = false;
12842 foreach ( $all_plugin_licenses as $license ) {
12843 if ( $license->id == $this->_site->license_id ) {
12844 // License found.
12845 $this->_license = $license;
12846 $license_found = true;
12847 break;
12848 }
12849 }
12850
12851 if ( $license_found ) {
12852 $this->link_license_2_user( $this->_license->id, $this->_user->id );
12853 }
12854 }
12855
12856 $this->_licenses = $this->get_user_licenses( $this->_user->id );
12857 }
12858 }
12859
12860 unset( $this->_site_api );
12861 unset( $this->_user_api );
12862 }
12863
12864 /**
12865 * Restore the blog context to the blog that originally loaded the module.
12866 *
12867 * @author Vova Feldman (@svovaf)
12868 * @since 2.0.0
12869 */
12870 function restore_current_blog() {
12871 $this->switch_to_blog( $this->_blog_id );
12872 }
12873
12874 /**
12875 * @author Vova Feldman (@svovaf)
12876 * @since 2.0.0
12877 *
12878 * @param array|WP_Site $site
12879 *
12880 * @return int
12881 */
12882 static function get_site_blog_id( &$site ) {
12883 return ( $site instanceof WP_Site ) ?
12884 $site->blog_id :
12885 ( is_object( $site ) && isset( $site->userblog_id ) ?
12886 $site->userblog_id :
12887 $site['blog_id'] );
12888 }
12889
12890 /**
12891 * @author Leo Fajardo (@leorw)
12892 * @since 2.0.0
12893 *
12894 * @param array|WP_Site|null $site
12895 *
12896 * @return array
12897 */
12898 function get_site_info( $site = null ) {
12899 $this->_logger->entrance();
12900
12901 $switched = false;
12902
12903 if ( is_null( $site ) ) {
12904 $url = get_site_url();
12905 $name = get_bloginfo( 'name' );
12906 $blog_id = null;
12907 } else {
12908 $blog_id = self::get_site_blog_id( $site );
12909
12910 if ( get_current_blog_id() != $blog_id ) {
12911 switch_to_blog( $blog_id );
12912 $switched = true;
12913 }
12914
12915 if ( $site instanceof WP_Site ) {
12916 $url = $site->siteurl;
12917 $name = $site->blogname;
12918 } else {
12919 $url = get_site_url( $blog_id );
12920 $name = get_bloginfo( 'name' );
12921 }
12922 }
12923
12924 $info = array(
12925 'uid' => $this->get_anonymous_id( $blog_id ),
12926 'url' => $url,
12927 'title' => $name,
12928 'language' => get_bloginfo( 'language' ),
12929 'charset' => get_bloginfo( 'charset' ),
12930 );
12931
12932 if ( is_numeric( $blog_id ) ) {
12933 $info['blog_id'] = $blog_id;
12934 }
12935
12936 if ( $switched ) {
12937 restore_current_blog();
12938 }
12939
12940 return $info;
12941 }
12942
12943 /**
12944 * Load the module's install based on the blog ID.
12945 *
12946 * @author Vova Feldman (@svovaf)
12947 * @since 2.0.0
12948 *
12949 * @param int|null $blog_id
12950 *
12951 * @return FS_Site
12952 */
12953 function get_install_by_blog_id( $blog_id = null ) {
12954 $installs = self::get_all_sites( $this->_module_type, $blog_id );
12955 $install = isset( $installs[ $this->_slug ] ) ? $installs[ $this->_slug ] : null;
12956
12957 if ( is_object( $install ) &&
12958 is_numeric( $install->id ) &&
12959 is_numeric( $install->user_id ) &&
12960 FS_Plugin_Plan::is_valid_id( $install->plan_id )
12961 ) {
12962 // Load site.
12963 $install = clone $install;
12964 }
12965
12966 return $install;
12967 }
12968
12969 /**
12970 * Check if module is installed on a specified site.
12971 *
12972 * @author Vova Feldman (@svovaf)
12973 * @since 2.0.0
12974 *
12975 * @param int|null $blog_id
12976 *
12977 * @return bool
12978 */
12979 function is_installed_on_site( $blog_id = null ) {
12980 $installs = self::get_all_sites( $this->_module_type, $blog_id );
12981 $install = isset( $installs[ $this->_slug ] ) ? $installs[ $this->_slug ] : null;
12982
12983 return (
12984 is_object( $install ) &&
12985 is_numeric( $install->id ) &&
12986 is_numeric( $install->user_id ) &&
12987 FS_Plugin_Plan::is_valid_id( $install->plan_id )
12988 );
12989 }
12990
12991 /**
12992 * Check if super-admin connected at least one site via the network opt-in.
12993 *
12994 * @author Vova Feldman (@svovaf)
12995 * @since 2.0.0
12996 *
12997 * @return bool
12998 */
12999 function is_network_registered() {
13000 if ( ! $this->_is_network_active ) {
13001 return false;
13002 }
13003
13004 return FS_User::is_valid_id( $this->_storage->network_user_id );
13005 }
13006
13007 /**
13008 * Returns the main user associated with the network.
13009 *
13010 * @author Vova Feldman (@svovaf)
13011 * @since 2.0.0
13012 *
13013 * @return FS_User
13014 */
13015 function get_network_user() {
13016 if ( ! $this->_is_network_active ) {
13017 return null;
13018 }
13019
13020 return FS_User::is_valid_id( $this->_storage->network_user_id ) ?
13021 self::_get_user_by_id( $this->_storage->network_user_id ) :
13022 null;
13023 }
13024
13025 /**
13026 * Returns the current context user or the network's main user.
13027 *
13028 * @author Vova Feldman (@svovaf)
13029 * @since 2.0.0
13030 *
13031 * @return FS_User
13032 */
13033 function get_current_or_network_user() {
13034 return ( $this->_user instanceof FS_User ) ?
13035 $this->_user :
13036 $this->get_network_user();
13037 }
13038
13039 /**
13040 * Returns the main install associated with the network.
13041 *
13042 * @author Vova Feldman (@svovaf)
13043 * @since 2.0.0
13044 *
13045 * @return FS_Site
13046 */
13047 function get_network_install() {
13048 if ( ! $this->_is_network_active ) {
13049 return null;
13050 }
13051
13052 return FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ?
13053 $this->get_install_by_blog_id( $this->_storage->network_install_blog_id ) :
13054 null;
13055 }
13056
13057 /**
13058 * Returns the blog ID that is associated with the main install.
13059 *
13060 * @author Leo Fajardo (@leorw)
13061 * @since 2.0.0
13062 *
13063 * @return int|null
13064 */
13065 function get_network_install_blog_id() {
13066 if ( ! $this->_is_network_active ) {
13067 return null;
13068 }
13069
13070 return FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ?
13071 $this->_storage->network_install_blog_id :
13072 null;
13073 }
13074
13075 /**
13076 * Returns the current context install or the network's main install.
13077 *
13078 * @author Vova Feldman (@svovaf)
13079 * @since 2.0.0
13080 *
13081 * @return FS_Site
13082 */
13083 function get_current_or_network_install() {
13084 return ( $this->_site instanceof FS_Site ) ?
13085 $this->_site :
13086 $this->get_network_install();
13087 }
13088
13089 /**
13090 * Check if executing a site level action from the network level admin.
13091 *
13092 * @author Vova Feldman (@svovaf)
13093 * @since 2.0.0
13094 *
13095 * @return false|int If yes, return the requested blog ID.
13096 */
13097 private function is_network_level_site_specific_action() {
13098 if ( ! $this->_is_network_active ) {
13099 return false;
13100 }
13101
13102 if ( ! fs_is_network_admin() ) {
13103 return false;
13104 }
13105
13106 $blog_id = fs_request_get( 'blog_id', '' );
13107
13108 return is_numeric( $blog_id ) ? $blog_id : false;
13109 }
13110
13111 /**
13112 * Check if executing an action from the network level admin.
13113 *
13114 * @author Vova Feldman (@svovaf)
13115 * @since 2.0.0
13116 *
13117 * @return bool
13118 */
13119 private function is_network_level_action() {
13120 return ( $this->_is_network_active && fs_is_network_admin() );
13121 }
13122
13123 /**
13124 * Needs to be executed after site deactivation, archive, deletion, or flag as spam.
13125 * 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.
13126 *
13127 * @author Vova Feldman (@svovaf)
13128 * @since 2.0.0
13129 *
13130 * @param int $context_blog_id
13131 */
13132 private function update_multisite_data_after_site_deactivation( $context_blog_id = 0 ) {
13133 $this->_logger->entrance();
13134
13135 if ( $this->_is_network_active ) {
13136 if ( $context_blog_id == $this->_storage->network_install_blog_id ) {
13137 $installs_map = $this->get_blog_install_map();
13138
13139 foreach ( $installs_map as $blog_id => $install ) {
13140 /**
13141 * @var FS_Site $install
13142 */
13143 if ( $context_blog_id == $blog_id ) {
13144 continue;
13145 }
13146
13147 if ( $install->user_id != $this->_storage->network_user_id ) {
13148 continue;
13149 }
13150
13151 // Switch reference to a blog that is opted-in and belong to the same super-admin.
13152 $this->_storage->network_install_blog_id = $blog_id;
13153 break;
13154 }
13155 }
13156 }
13157
13158 if ( $this->is_sync_cron_scheduled() &&
13159 $context_blog_id == $this->get_sync_cron_blog_id()
13160 ) {
13161 $this->schedule_sync_cron( WP_FS__SCRIPT_START_TIME, true, $context_blog_id );
13162 }
13163
13164 if ( $this->is_install_sync_scheduled() &&
13165 $context_blog_id == $this->get_install_sync_cron_blog_id()
13166 ) {
13167 $this->schedule_install_sync( $context_blog_id );
13168 }
13169 }
13170
13171 /**
13172 * Executed after site deactivation, archive, or flag as spam.
13173 *
13174 * @author Vova Feldman (@svovaf)
13175 * @since 2.0.0
13176 *
13177 * @param int $context_blog_id
13178 */
13179 public function _after_site_deactivated_callback( $context_blog_id = 0 ) {
13180 $this->_logger->entrance();
13181
13182 $install = $this->get_install_by_blog_id( $context_blog_id );
13183
13184 if ( ! is_object( $install ) ) {
13185 // Site not connected.
13186 return;
13187 }
13188
13189 $this->update_multisite_data_after_site_deactivation( $context_blog_id );
13190
13191 $current_blog_id = get_current_blog_id();
13192
13193 $this->switch_to_blog( $context_blog_id );
13194
13195 // Send deactivation event.
13196 $this->sync_install( array(
13197 'is_active' => false,
13198 ) );
13199
13200 $this->switch_to_blog( $current_blog_id );
13201 }
13202
13203 /**
13204 * Executed after site deletion.
13205 *
13206 * @author Vova Feldman (@svovaf)
13207 * @since 2.0.0
13208 *
13209 * @param int $context_blog_id
13210 * @param bool $drop True if site's database tables should be dropped. Default is false.
13211 */
13212 public function _after_site_deleted_callback( $context_blog_id = 0, $drop = false ) {
13213 $this->_logger->entrance();
13214
13215 $install = $this->get_install_by_blog_id( $context_blog_id );
13216
13217 if ( ! is_object( $install ) ) {
13218 // Site not connected.
13219 return;
13220 }
13221
13222 $this->update_multisite_data_after_site_deactivation( $context_blog_id );
13223
13224 $current_blog_id = get_current_blog_id();
13225
13226 $this->switch_to_blog( $context_blog_id );
13227
13228 if ( $drop ) {
13229 // Delete install if dropping site DB.
13230 $this->delete_account_event();
13231 } else {
13232 // Send deactivation event.
13233 $this->sync_install( array(
13234 'is_active' => false,
13235 ) );
13236 }
13237
13238 $this->switch_to_blog( $current_blog_id );
13239 }
13240
13241 /**
13242 * Executed after site re-activation.
13243 *
13244 * @author Vova Feldman (@svovaf)
13245 * @since 2.0.0
13246 *
13247 * @param int $context_blog_id
13248 */
13249 public function _after_site_reactivated_callback( $context_blog_id = 0 ) {
13250 $this->_logger->entrance();
13251
13252 $install = $this->get_install_by_blog_id( $context_blog_id );
13253
13254 if ( ! is_object( $install ) ) {
13255 // Site not connected.
13256 return;
13257 }
13258
13259 if ( ! self::is_site_active( $context_blog_id ) ) {
13260 // Site not yet active (can be in spam mode, archived, deleted...).
13261 return;
13262 }
13263
13264 $current_blog_id = get_current_blog_id();
13265
13266 $this->switch_to_blog( $context_blog_id );
13267
13268 // Send re-activation event.
13269 $this->sync_install( array(
13270 'is_active' => true,
13271 ) );
13272
13273 $this->switch_to_blog( $current_blog_id );
13274 }
13275
13276 #endregion Multisite
13277
13278 /**
13279 * @author Leo Fajardo (@leorw)
13280 *
13281 * @param string $path
13282 * @param string $scheme
13283 * @param bool $network
13284 *
13285 * @return string
13286 */
13287 private function admin_url( $path = '', $scheme = 'admin', $network = true ) {
13288 return ( $this->_is_network_active && $network ) ?
13289 network_admin_url( $path, $scheme ) :
13290 admin_url( $path, $scheme );
13291 }
13292
13293 /**
13294 * Check if currently in a specified admin page.
13295 *
13296 * @author Vova Feldman (@svovaf)
13297 * @since 1.2.2.7
13298 *
13299 * @param string $page
13300 *
13301 * @return bool
13302 */
13303 function is_admin_page( $page ) {
13304 return ( $this->_menu->get_slug( $page ) === fs_request_get( 'page', '', 'get' ) );
13305 }
13306
13307 /**
13308 * Get module's main admin setting page URL.
13309 *
13310 * @author Vova Feldman (@svovaf)
13311 * @since 1.2.2.7
13312 *
13313 * @return string
13314 */
13315 function main_menu_url() {
13316 return $this->_menu->main_menu_url();
13317 }
13318
13319 /**
13320 * Check if currently on the theme's setting page or
13321 * on any of the Freemius added pages (via tabs).
13322 *
13323 * @author Vova Feldman (@svovaf)
13324 * @since 1.2.2.7
13325 *
13326 * @return bool
13327 */
13328 function is_theme_settings_page() {
13329 return fs_starts_with(
13330 fs_request_get( 'page', '', 'get' ),
13331 $this->_menu->get_slug()
13332 );
13333 }
13334
13335 /**
13336 * Plugin's account page + sync license URL.
13337 *
13338 * @author Vova Feldman (@svovaf)
13339 * @since 1.1.9.1
13340 *
13341 * @param bool|number $plugin_id
13342 * @param bool $add_action_nonce
13343 * @param array $params
13344 *
13345 * @return string
13346 */
13347 function _get_sync_license_url( $plugin_id = false, $add_action_nonce = true, $params = array() ) {
13348 if ( is_numeric( $plugin_id ) ) {
13349 $params['plugin_id'] = $plugin_id;
13350 }
13351
13352 return $this->get_account_url(
13353 $this->get_unique_affix() . '_sync_license',
13354 $params,
13355 $add_action_nonce
13356 );
13357 }
13358
13359 /**
13360 * Plugin's account URL.
13361 *
13362 * @author Vova Feldman (@svovaf)
13363 * @since 1.0.4
13364 *
13365 * @param bool|string $action
13366 * @param array $params
13367 *
13368 * @param bool $add_action_nonce
13369 *
13370 * @return string
13371 */
13372 function get_account_url( $action = false, $params = array(), $add_action_nonce = true ) {
13373 if ( is_string( $action ) ) {
13374 $params['fs_action'] = $action;
13375 }
13376
13377 self::require_pluggable_essentials();
13378
13379 return ( $add_action_nonce && is_string( $action ) ) ?
13380 fs_nonce_url( $this->_get_admin_page_url( 'account', $params ), $action ) :
13381 $this->_get_admin_page_url( 'account', $params );
13382 }
13383
13384 /**
13385 * @author Vova Feldman (@svovaf)
13386 * @since 1.2.0
13387 *
13388 * @param string $tab
13389 * @param bool $action
13390 * @param array $params
13391 * @param bool $add_action_nonce
13392 *
13393 * @return string
13394 *
13395 * @uses get_account_url()
13396 */
13397 function get_account_tab_url( $tab, $action = false, $params = array(), $add_action_nonce = true ) {
13398 $params['tab'] = $tab;
13399
13400 return $this->get_account_url( $action, $params, $add_action_nonce );
13401 }
13402
13403 /**
13404 * Plugin's account URL.
13405 *
13406 * @author Vova Feldman (@svovaf)
13407 * @since 1.0.4
13408 *
13409 * @param bool|string $topic
13410 * @param bool|string $message
13411 *
13412 * @return string
13413 */
13414 function contact_url( $topic = false, $message = false ) {
13415 $params = array();
13416 if ( is_string( $topic ) ) {
13417 $params['topic'] = $topic;
13418 }
13419 if ( is_string( $message ) ) {
13420 $params['message'] = $message;
13421 }
13422
13423 if ( $this->is_addon() ) {
13424 $params['addon_id'] = $this->get_id();
13425
13426 return $this->get_parent_instance()->_get_admin_page_url( 'contact', $params );
13427 } else {
13428 return $this->_get_admin_page_url( 'contact', $params );
13429 }
13430 }
13431
13432 /**
13433 * Add-on direct info URL.
13434 *
13435 * @author Vova Feldman (@svovaf)
13436 * @since 1.1.0
13437 *
13438 * @param string $slug
13439 *
13440 * @return string
13441 */
13442 function addon_url( $slug ) {
13443 return $this->_get_admin_page_url( 'addons', array(
13444 'slug' => $slug
13445 ) );
13446 }
13447
13448 /* Logger
13449 ------------------------------------------------------------------------------------------------------------------*/
13450 /**
13451 * @param string $id
13452 * @param bool $prefix_slug
13453 *
13454 * @return FS_Logger
13455 */
13456 function get_logger( $id = '', $prefix_slug = true ) {
13457 return FS_Logger::get_logger( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id );
13458 }
13459
13460 /**
13461 * Note: This method is used externally so don't delete it.
13462 *
13463 * @param $id
13464 * @param bool $load_options
13465 * @param bool $prefix_slug
13466 *
13467 * @return FS_Option_Manager
13468 */
13469 function get_options_manager( $id, $load_options = false, $prefix_slug = true ) {
13470 return FS_Option_Manager::get_manager( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id, $load_options );
13471 }
13472
13473 /* Security
13474 ------------------------------------------------------------------------------------------------------------------*/
13475 private static function _encrypt( $str ) {
13476 if ( is_null( $str ) ) {
13477 return null;
13478 }
13479
13480 /**
13481 * The encrypt/decrypt functions are used to protect
13482 * the user from messing up with some of the sensitive
13483 * data stored for the module as a JSON in the database.
13484 *
13485 * I used the same suggested hack by the theme review team.
13486 * For more details, look at the function `Base64UrlDecode()`
13487 * in `./sdk/FreemiusBase.php`.
13488 *
13489 * @todo Remove this hack once the base64 error is removed from the Theme Check.
13490 *
13491 * @author Vova Feldman (@svovaf)
13492 * @since 1.2.2
13493 */
13494 $fn = 'base64' . '_encode';
13495
13496 return $fn( $str );
13497 }
13498
13499 static function _decrypt( $str ) {
13500 if ( is_null( $str ) ) {
13501 return null;
13502 }
13503
13504 /**
13505 * The encrypt/decrypt functions are used to protect
13506 * the user from messing up with some of the sensitive
13507 * data stored for the module as a JSON in the database.
13508 *
13509 * I used the same suggested hack by the theme review team.
13510 * For more details, look at the function `Base64UrlDecode()`
13511 * in `./sdk/FreemiusBase.php`.
13512 *
13513 * @todo Remove this hack once the base64 error is removed from the Theme Check.
13514 *
13515 * @author Vova Feldman (@svovaf)
13516 * @since 1.2.2
13517 */
13518 $fn = 'base64' . '_decode';
13519
13520 return $fn( $str );
13521 }
13522
13523 /**
13524 * @author Vova Feldman (@svovaf)
13525 * @since 1.0.5
13526 *
13527 * @param FS_Entity $entity
13528 *
13529 * @return FS_Entity Return an encrypted clone entity.
13530 */
13531 private static function _encrypt_entity( FS_Entity $entity ) {
13532 $clone = clone $entity;
13533 $props = get_object_vars( $entity );
13534
13535 foreach ( $props as $key => $val ) {
13536 $clone->{$key} = self::_encrypt( $val );
13537 }
13538
13539 return $clone;
13540 }
13541
13542 /**
13543 * @author Vova Feldman (@svovaf)
13544 * @since 1.0.5
13545 *
13546 * @param FS_Entity $entity
13547 *
13548 * @return FS_Entity Return an decrypted clone entity.
13549 */
13550 private static function decrypt_entity( FS_Entity $entity ) {
13551 $clone = clone $entity;
13552 $props = get_object_vars( $entity );
13553
13554 foreach ( $props as $key => $val ) {
13555 $clone->{$key} = self::_decrypt( $val );
13556 }
13557
13558 return $clone;
13559 }
13560
13561 /**
13562 * Tries to activate account based on POST params.
13563 *
13564 * @author Vova Feldman (@svovaf)
13565 * @since 1.0.2
13566 *
13567 * @deprecated Not in use, outdated.
13568 */
13569 function _activate_account() {
13570 if ( $this->is_registered() ) {
13571 // Already activated.
13572 return;
13573 }
13574
13575 self::_clean_admin_content_section();
13576
13577 if ( fs_request_is_action( 'activate' ) && fs_request_is_post() ) {
13578 // check_admin_referer( 'activate_' . $this->_plugin->public_key );
13579
13580 // Verify matching plugin details.
13581 if ( $this->_plugin->id != fs_request_get( 'plugin_id' ) || $this->_slug != fs_request_get( 'plugin_slug' ) ) {
13582 return;
13583 }
13584
13585 $user = new FS_User();
13586 $user->id = fs_request_get( 'user_id' );
13587 $user->public_key = fs_request_get( 'user_public_key' );
13588 $user->secret_key = fs_request_get( 'user_secret_key' );
13589 $user->email = fs_request_get( 'user_email' );
13590 $user->first = fs_request_get( 'user_first' );
13591 $user->last = fs_request_get( 'user_last' );
13592 $user->is_verified = fs_request_get_bool( 'user_is_verified' );
13593
13594 $site = new FS_Site();
13595 $site->id = fs_request_get( 'install_id' );
13596 $site->public_key = fs_request_get( 'install_public_key' );
13597 $site->secret_key = fs_request_get( 'install_secret_key' );
13598 $site->plan_id = fs_request_get( 'plan_id' );
13599
13600 $plans = array();
13601 $plans_data = json_decode( urldecode( fs_request_get( 'plans' ) ) );
13602 foreach ( $plans_data as $p ) {
13603 $plan = new FS_Plugin_Plan( $p );
13604 if ( $site->plan_id == $plan->id ) {
13605 $plan->title = fs_request_get( 'plan_title' );
13606 $plan->name = fs_request_get( 'plan_name' );
13607 }
13608
13609 $plans[] = $plan;
13610 }
13611
13612 $this->_set_account( $user, $site, $plans );
13613
13614 // Reload the page with the keys.
13615 fs_redirect( $this->_get_admin_page_url() );
13616 }
13617 }
13618
13619 /**
13620 * @author Vova Feldman (@svovaf)
13621 * @since 1.0.7
13622 *
13623 * @param string $email
13624 *
13625 * @return FS_User|false
13626 */
13627 static function _get_user_by_email( $email ) {
13628 self::$_static_logger->entrance();
13629
13630 $email = trim( strtolower( $email ) );
13631
13632 $users = self::get_all_users();
13633
13634 if ( is_array( $users ) ) {
13635 foreach ( $users as $user ) {
13636 if ( $email === trim( strtolower( $user->email ) ) ) {
13637 return $user;
13638 }
13639 }
13640 }
13641
13642 return false;
13643 }
13644
13645 #----------------------------------------------------------------------------------
13646 #region Account (Loading, Updates & Activation)
13647 #----------------------------------------------------------------------------------
13648
13649 /***
13650 * Load account information (user + site).
13651 *
13652 * @author Vova Feldman (@svovaf)
13653 * @since 1.0.1
13654 */
13655 private function _load_account() {
13656 $this->_logger->entrance();
13657
13658 $this->do_action( 'before_account_load' );
13659
13660 $users = self::get_all_users();
13661 $plans = self::get_all_plans( $this->_module_type );
13662
13663 if ( $this->_logger->is_on() && is_admin() ) {
13664 $this->_logger->log( 'users = ' . var_export( $users, true ) );
13665 $this->_logger->log( 'plans = ' . var_export( $plans, true ) );
13666 }
13667
13668 $site = fs_is_network_admin() ?
13669 $this->get_network_install() :
13670 $this->get_install_by_blog_id();
13671
13672 if ( fs_is_network_admin() &&
13673 ! is_object( $site ) &&
13674 FS_Site::is_valid_id( $this->_storage->network_install_blog_id )
13675 ) {
13676 $first_install = $this->find_first_install();
13677
13678 if ( is_null( $first_install ) ) {
13679 unset( $this->_storage->network_install_blog_id );
13680 } else {
13681 $site = $first_install['install'];
13682 $this->_storage->network_install_blog_id = $first_install['blog_id'];
13683 }
13684 }
13685
13686 if ( is_object( $site ) &&
13687 is_numeric( $site->id ) &&
13688 is_numeric( $site->user_id ) &&
13689 FS_Plugin_Plan::is_valid_id( $site->plan_id )
13690 ) {
13691 // Load site.
13692 $this->_site = $site;
13693
13694 // Load plans.
13695 $this->_plans = $plans[ $this->_slug ];
13696 if ( ! is_array( $this->_plans ) || empty( $this->_plans ) ) {
13697 $this->_sync_plans();
13698 } else {
13699 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
13700 if ( $this->_plans[ $i ] instanceof FS_Plugin_Plan ) {
13701 $this->_plans[ $i ] = self::decrypt_entity( $this->_plans[ $i ] );
13702 } else {
13703 unset( $this->_plans[ $i ] );
13704 }
13705 }
13706 }
13707 }
13708
13709 $user = null;
13710 if ( fs_is_network_admin() && $this->_is_network_active ) {
13711 $user = $this->get_network_user();
13712 }
13713
13714 if ( is_object( $user ) ) {
13715 $this->_user = clone $user;
13716 } else if ( $this->_site ) {
13717 $user = self::_get_user_by_id( $this->_site->user_id );
13718
13719 if ( ! is_object( $user ) && FS_User::is_valid_id( $this->_storage->prev_user_id ) ) {
13720 /**
13721 * Try to load the previous owner. This recovery is used for the following use-case:
13722 * 1. Opt-in
13723 * 2. Cloning site1 to site2
13724 * 3. Ownership switch in site1 (same applies for site2)
13725 * 4. Install data sync on site2
13726 * 5. Now site2's install is associated with the new owner which does not exists locally.
13727 */
13728 $user = self::_get_user_by_id( $this->_storage->prev_user_id );
13729 }
13730
13731 if ( ! is_object( $user ) ) {
13732 /**
13733 * This is a special fault tolerance mechanism to handle a scenario that the user data is missing.
13734 */
13735 $user = $this->fetch_user_by_install();
13736 }
13737
13738 $this->_user = ( $user instanceof FS_User ) ?
13739 clone $user :
13740 null;
13741 }
13742
13743 if ( is_object( $this->_user ) ) {
13744 // Load licenses.
13745 $this->_licenses = $this->get_user_licenses( $this->_user->id );
13746 }
13747
13748 if ( is_object( $this->_site ) ) {
13749 $this->_license = $this->_get_license_by_id( $this->_site->license_id );
13750
13751 if ( $this->_site->version != $this->get_plugin_version() ) {
13752 // If stored install version is different than current installed plugin version,
13753 // then update plugin version event.
13754 $this->update_plugin_version_event();
13755 }
13756 }
13757
13758 if ( true === $this->_storage->require_license_activation &&
13759 ! fs_request_get_bool( 'require_license', true )
13760 ) {
13761 $this->_storage->require_license_activation = false;
13762 }
13763
13764 if ( $this->is_theme() ) {
13765 $this->_register_account_hooks();
13766 }
13767 }
13768
13769 /**
13770 * Special user recovery mechanism.
13771 *
13772 * @author Vova Feldman (@svovaf)
13773 * @since 2.0.0
13774 *
13775 * @return \FS_User|mixed
13776 */
13777 private function fetch_user_by_install() {
13778 $api = $this->get_api_site_scope();
13779
13780 $uid = $this->get_anonymous_id();
13781 $request_path = "/users/{$this->_site->user_id}.json?uid={$uid}";
13782
13783 $result = $api->get( $request_path, false, WP_FS__TIME_10_MIN_IN_SEC );
13784
13785 if ( $this->is_api_result_entity( $result ) ) {
13786 $user = new FS_User( $result );
13787 $this->_user = $user;
13788 $this->_store_user();
13789
13790 return $user;
13791 }
13792
13793 $error_code = FS_Api::get_error_code( $result );
13794
13795 if ( in_array( $error_code, array( 'invalid_unique_id', 'user_cannot_be_recovered' ) ) ) {
13796 /**
13797 * Those API errors will continue coming and are not recoverable with the
13798 * current site's data. Therefore, extend the API call's cached result to 7 days.
13799 */
13800 $api->update_cache_expiration( $request_path, WP_FS__TIME_WEEK_IN_SEC );
13801 }
13802
13803 return $result;
13804 }
13805
13806 /**
13807 * @author Vova Feldman (@svovaf)
13808 * @since 1.0.1
13809 *
13810 * @param FS_User $user
13811 * @param FS_Site $site
13812 * @param bool|array $plans
13813 */
13814 private function _set_account( FS_User $user, FS_Site $site, $plans = false ) {
13815 $site->user_id = $user->id;
13816
13817 $this->_site = $site;
13818 $this->_user = $user;
13819 if ( false !== $plans ) {
13820 $this->_plans = $plans;
13821 }
13822
13823 $this->send_install_update();
13824
13825 $this->_store_account();
13826
13827 }
13828
13829 /**
13830 * Get a sanitized array with the WordPress version, SDK version, and PHP version.
13831 * Each version is trimmed after the 16th char.
13832 *
13833 * @author Vova Feldman (@svovaf)
13834 * @since 2.2.1
13835 *
13836 * @return array
13837 */
13838 private function get_versions() {
13839 $versions = array();
13840 $versions['platform_version'] = get_bloginfo( 'version' );
13841 $versions['sdk_version'] = $this->version;
13842 $versions['programming_language_version'] = phpversion();
13843
13844 foreach ( $versions as $k => $version ) {
13845 if ( is_string( $versions[ $k ] ) && ! empty( $versions[ $k ] ) ) {
13846 $versions[ $k ] = substr( $versions[ $k ], 0, 16 );
13847 }
13848 }
13849
13850 return $versions;
13851 }
13852
13853 /**
13854 * @author Vova Feldman (@svovaf)
13855 * @since 1.1.7.4
13856 *
13857 * @param array $override_with
13858 * @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.
13859 *
13860 * @return array
13861 */
13862 function get_opt_in_params( $override_with = array(), $network_level_or_blog_id = null ) {
13863 $this->_logger->entrance();
13864
13865 $current_user = self::_get_current_wp_user();
13866
13867 $activation_action = $this->get_unique_affix() . '_activate_new';
13868 $return_url = $this->is_anonymous() ?
13869 // If skipped already, then return to the account page.
13870 $this->get_account_url( $activation_action, array(), false ) :
13871 // Return to the module's main page.
13872 $this->get_after_activation_url( 'after_connect_url', array( 'fs_action' => $activation_action ) );
13873
13874 $versions = $this->get_versions();
13875
13876 $params = array_merge( $versions, array(
13877 'user_firstname' => $current_user->user_firstname,
13878 'user_lastname' => $current_user->user_lastname,
13879 'user_nickname' => $current_user->user_nicename,
13880 'user_email' => $current_user->user_email,
13881 'user_ip' => WP_FS__REMOTE_ADDR,
13882 'plugin_slug' => $this->_slug,
13883 'plugin_id' => $this->get_id(),
13884 'plugin_public_key' => $this->get_public_key(),
13885 'plugin_version' => $this->get_plugin_version(),
13886 'return_url' => fs_nonce_url( $return_url, $activation_action ),
13887 'account_url' => fs_nonce_url( $this->_get_admin_page_url(
13888 'account',
13889 array( 'fs_action' => 'sync_user' )
13890 ), 'sync_user' ),
13891 'is_premium' => $this->is_premium(),
13892 'is_active' => true,
13893 'is_uninstalled' => false,
13894 ) );
13895
13896 if ( true === $network_level_or_blog_id ) {
13897 if ( ! isset( $override_with['sites'] ) ) {
13898 $params['sites'] = array();
13899
13900 $sites = self::get_sites();
13901
13902 foreach ( $sites as $site ) {
13903 $blog_id = self::get_site_blog_id( $site );
13904 if ( ! $this->is_site_delegated_connection( $blog_id ) &&
13905 ! $this->is_installed_on_site( $blog_id )
13906 ) {
13907 $params['sites'][] = $this->get_site_info( $site );
13908 }
13909 }
13910 }
13911 } else {
13912 $site = is_numeric( $network_level_or_blog_id ) ?
13913 array( 'blog_id' => $network_level_or_blog_id ) :
13914 null;
13915
13916 $site = $this->get_site_info( $site );
13917
13918 $params = array_merge( $params, array(
13919 'site_uid' => $site['uid'],
13920 'site_url' => $site['url'],
13921 'site_name' => $site['title'],
13922 'language' => $site['language'],
13923 'charset' => $site['charset'],
13924 ) );
13925 }
13926
13927 if ( $this->is_pending_activation() &&
13928 ! empty( $this->_storage->pending_license_key )
13929 ) {
13930 $params['license_key'] = $this->_storage->pending_license_key;
13931 }
13932
13933 if ( WP_FS__SKIP_EMAIL_ACTIVATION && $this->has_secret_key() ) {
13934 // Even though rand() is known for its security issues,
13935 // the timestamp adds another layer of protection.
13936 // It would be very hard for an attacker to get the secret key form here.
13937 // Plus, this should never run in production since the secret should never
13938 // be included in the production version.
13939 $params['ts'] = WP_FS__SCRIPT_START_TIME;
13940 $params['salt'] = md5( uniqid( rand() ) );
13941 $params['secure'] = md5(
13942 $params['ts'] .
13943 $params['salt'] .
13944 $this->get_secret_key()
13945 );
13946 }
13947
13948 return array_merge( $params, $override_with );
13949 }
13950
13951 /**
13952 * 1. If successful opt-in or pending activation returns the next page that the user should be redirected to.
13953 * 2. If there was an API error, return the API result.
13954 *
13955 * @author Vova Feldman (@svovaf)
13956 * @since 1.1.7.4
13957 *
13958 * @param string|bool $email
13959 * @param string|bool $first
13960 * @param string|bool $last
13961 * @param string|bool $license_key
13962 * @param bool $is_uninstall If "true", this means that the module is currently being uninstalled.
13963 * In this case, the user and site info will be sent to the server but no
13964 * data will be saved to the WP installation's database.
13965 * @param number|bool $trial_plan_id
13966 * @param bool $is_disconnected Whether or not to opt in without tracking.
13967 * @param null|bool $is_marketing_allowed
13968 * @param array $sites If network-level opt-in, an array of containing details of sites.
13969 *
13970 * @return string|object
13971 * @use WP_Error
13972 */
13973 function opt_in(
13974 $email = false,
13975 $first = false,
13976 $last = false,
13977 $license_key = false,
13978 $is_uninstall = false,
13979 $trial_plan_id = false,
13980 $is_disconnected = false,
13981 $is_marketing_allowed = null,
13982 $sites = array()
13983 ) {
13984 $this->_logger->entrance();
13985
13986 if ( false === $email ) {
13987 $current_user = self::_get_current_wp_user();
13988 $email = $current_user->user_email;
13989 }
13990
13991 /**
13992 * @since 1.2.1 If activating with license key, ignore the context-user
13993 * since the user will be automatically loaded from the license.
13994 */
13995 if ( empty( $license_key ) ) {
13996 // Clean up pending license if opt-ing in again.
13997 $this->_storage->remove( 'pending_license_key' );
13998
13999 if ( ! $is_uninstall ) {
14000 $fs_user = Freemius::_get_user_by_email( $email );
14001 if ( is_object( $fs_user ) && ! $this->is_pending_activation() ) {
14002 return $this->install_with_current_user(
14003 false,
14004 $trial_plan_id,
14005 $sites
14006 );
14007 }
14008 }
14009 }
14010
14011 $user_info = array();
14012 if ( ! empty( $email ) ) {
14013 $user_info['user_email'] = $email;
14014 }
14015 if ( ! empty( $first ) ) {
14016 $user_info['user_firstname'] = $first;
14017 }
14018 if ( ! empty( $last ) ) {
14019 $user_info['user_lastname'] = $last;
14020 }
14021
14022 if ( ! empty( $sites ) ) {
14023 $is_network = true;
14024
14025 $user_info['sites'] = $sites;
14026 } else {
14027 $is_network = false;
14028 }
14029
14030 $params = $this->get_opt_in_params( $user_info, $is_network );
14031
14032 $filtered_license_key = false;
14033 if ( is_string( $license_key ) ) {
14034 $filtered_license_key = $this->apply_filters( 'license_key', $license_key );
14035 $params['license_key'] = $filtered_license_key;
14036 } else if ( FS_Plugin_Plan::is_valid_id( $trial_plan_id ) ) {
14037 $params['trial_plan_id'] = $trial_plan_id;
14038 }
14039
14040 if ( $is_uninstall ) {
14041 $params['uninstall_params'] = array(
14042 'reason_id' => $this->_storage->uninstall_reason->id,
14043 'reason_info' => $this->_storage->uninstall_reason->info
14044 );
14045 }
14046
14047 if ( isset( $params['license_key'] ) ) {
14048 $fs_user = Freemius::_get_user_by_email( $email );
14049
14050 if ( is_object( $fs_user ) ) {
14051 /**
14052 * If opting in with a context license and the context WP Admin user already opted in
14053 * before from the current site, add the user context security params to avoid the
14054 * unnecessary email activation when the context license is owned by the same context user.
14055 *
14056 * @author Leo Fajardo (@leorw)
14057 * @since 1.2.3
14058 */
14059 $params = array_merge( $params, FS_Security::instance()->get_context_params(
14060 $fs_user,
14061 false,
14062 'install_with_existing_user'
14063 ) );
14064 }
14065 }
14066
14067 if ( is_bool( $is_marketing_allowed ) ) {
14068 $params['is_marketing_allowed'] = $is_marketing_allowed;
14069 }
14070
14071 $params['is_disconnected'] = $is_disconnected;
14072 $params['format'] = 'json';
14073
14074 $request = array(
14075 'method' => 'POST',
14076 'body' => $params,
14077 'timeout' => WP_FS__DEBUG_SDK ? 60 : 30,
14078 );
14079
14080 $url = $this->add_show_pending( WP_FS__ADDRESS . '/action/service/user/install/' );
14081 $response = self::safe_remote_post( $url, $request );
14082
14083 if ( is_wp_error( $response ) ) {
14084 /**
14085 * @var WP_Error $response
14086 */
14087 $result = new stdClass();
14088
14089 $error_code = $response->get_error_code();
14090 $error_type = str_replace( ' ', '', ucwords( str_replace( '_', ' ', $error_code ) ) );
14091
14092 $result->error = (object) array(
14093 'type' => $error_type,
14094 'message' => $response->get_error_message(),
14095 'code' => $error_code,
14096 'http' => 402
14097 );
14098
14099 return $result;
14100 }
14101
14102 // Module is being uninstalled, don't handle the returned data.
14103 if ( $is_uninstall ) {
14104 return true;
14105 }
14106
14107 /**
14108 * 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.
14109 *
14110 * @author Vova Feldman (@svovaf)
14111 * @since 1.2.3
14112 * @link https://themes.trac.wordpress.org/ticket/46134#comment:5
14113 * @link https://themes.trac.wordpress.org/ticket/46134#comment:9
14114 * @link https://themes.trac.wordpress.org/ticket/46134#comment:12
14115 * @link https://themes.trac.wordpress.org/ticket/46134#comment:14
14116 */
14117 $decoded = is_string( $response['body'] ) ?
14118 json_decode( $response['body'] ) :
14119 null;
14120
14121 if ( empty( $decoded ) ) {
14122 return false;
14123 }
14124
14125 if ( ! $this->is_api_result_object( $decoded ) ) {
14126 if ( ! empty( $params['license_key'] ) ) {
14127 // Pass the fully entered license key to the failure handler.
14128 $params['license_key'] = $license_key;
14129 }
14130
14131 return $is_uninstall ?
14132 $decoded :
14133 $this->apply_filters( 'after_install_failure', $decoded, $params );
14134 } else if ( isset( $decoded->pending_activation ) && $decoded->pending_activation ) {
14135 if ( $is_network ) {
14136 $site_ids = array();
14137 foreach ( $sites as $site ) {
14138 $site_ids[] = $site['blog_id'];
14139 }
14140
14141 /**
14142 * Store the sites so that they can be installed once the user has clicked on the activation link
14143 * in the email.
14144 *
14145 * @author Leo Fajardo (@leorw)
14146 */
14147 $this->_storage->pending_sites_info = array(
14148 'blog_ids' => $site_ids,
14149 'license_key' => $license_key,
14150 'trial_plan_id' => $trial_plan_id
14151 );
14152 }
14153
14154 // Pending activation, add message.
14155 return $this->set_pending_confirmation(
14156 ( isset( $decoded->email ) ?
14157 $decoded->email :
14158 true ),
14159 false,
14160 $filtered_license_key,
14161 ! empty( $params['trial_plan_id'] )
14162 );
14163 } else if ( isset( $decoded->install_secret_key ) ) {
14164 return $this->install_with_new_user(
14165 $decoded->user_id,
14166 $decoded->user_public_key,
14167 $decoded->user_secret_key,
14168 ( isset( $decoded->is_marketing_allowed ) && ! is_null( $decoded->is_marketing_allowed ) ?
14169 $decoded->is_marketing_allowed :
14170 null ),
14171 $decoded->install_id,
14172 $decoded->install_public_key,
14173 $decoded->install_secret_key,
14174 false
14175 );
14176 } else if ( is_array( $decoded->installs ) ) {
14177 return $this->install_many_with_new_user(
14178 $decoded->user_id,
14179 $decoded->user_public_key,
14180 $decoded->user_secret_key,
14181 ( isset( $decoded->is_marketing_allowed ) && ! is_null( $decoded->is_marketing_allowed ) ?
14182 $decoded->is_marketing_allowed :
14183 null ),
14184 $decoded->installs,
14185 false
14186 );
14187 }
14188
14189 return $decoded;
14190 }
14191
14192 /**
14193 * Set user and site identities.
14194 *
14195 * @author Vova Feldman (@svovaf)
14196 * @since 1.0.9
14197 *
14198 * @param FS_User $user
14199 * @param FS_Site $site
14200 * @param bool $redirect
14201 * @param bool $auto_install Since 1.2.1.7 If `true` and setting up an account with a valid license, will
14202 * redirect (or return a URL) to the account page with a special parameter to
14203 * trigger the auto installation processes.
14204 *
14205 * @return string If redirect is `false`, returns the next page the user should be redirected to.
14206 */
14207 function setup_account(
14208 FS_User $user,
14209 FS_Site $site,
14210 $redirect = true,
14211 $auto_install = false
14212 ) {
14213 return $this->setup_network_account(
14214 $user,
14215 array( $site ),
14216 $redirect,
14217 $auto_install,
14218 false
14219 );
14220 }
14221
14222 /**
14223 * Set user and site identities.
14224 *
14225 * @author Vova Feldman (@svovaf)
14226 * @since 2.0.0
14227 *
14228 * @param FS_User $user
14229 * @param FS_Site[] $installs
14230 * @param bool $redirect
14231 * @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.
14232 * @param bool $is_network_level_opt_in
14233 *
14234 * @return string If redirect is `false`, returns the next page the user should be redirected to.
14235 */
14236 function setup_network_account(
14237 FS_User $user,
14238 array $installs,
14239 $redirect = true,
14240 $auto_install = false,
14241 $is_network_level_opt_in = true
14242 ) {
14243 $first_install = $installs[0];
14244
14245 $this->_user = $user;
14246 $this->_site = $first_install;
14247
14248 $this->_sync_plans();
14249
14250 if ( $this->_storage->handle_gdpr_admin_notice &&
14251 $this->should_handle_gdpr_admin_notice() &&
14252 FS_GDPR_Manager::instance()->should_show_opt_in_notice()
14253 ) {
14254 /**
14255 * Clear user lock after an opt-in.
14256 */
14257 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
14258 FS_User_Lock::instance()->unlock();
14259 }
14260
14261 if ( 1 < count( $installs ) ) {
14262 // Only network level opt-in can have more than one install.
14263 $is_network_level_opt_in = true;
14264 }
14265 // $is_network_level_opt_in = self::is_ajax_action_static( 'network_activate', $this->_module_id );
14266 // If Freemius was OFF before, turn it on.
14267 $this->turn_on();
14268
14269 if ( ! $this->_is_network_active || ! $is_network_level_opt_in ) {
14270 $this->_set_account( $user, $first_install );
14271
14272 $this->do_action( 'after_account_connection', $user, $first_install );
14273 } else {
14274 $this->_store_user();
14275
14276 // Map site addresses to their blog IDs.
14277 $address_to_blog_map = $this->get_address_to_blog_map();
14278
14279 $first_blog_id = null;
14280 $blog_2_install_map = array();
14281 foreach ( $installs as $install ) {
14282 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
14283 $blog_id = $address_to_blog_map[ $address ];
14284
14285 $this->_store_site( true, $blog_id, $install );
14286
14287 if ( is_null( $first_blog_id ) ) {
14288 $first_blog_id = $blog_id;
14289 }
14290
14291 $blog_2_install_map[ $blog_id ] = $install;
14292 }
14293
14294 if ( ! FS_User::is_valid_id( $this->_storage->network_user_id ) ||
14295 ! is_object( self::_get_user_by_id( $this->_storage->network_user_id ) )
14296 ) {
14297 // Store network user.
14298 $this->_storage->network_user_id = $this->_user->id;
14299 }
14300
14301 if ( ! FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ) {
14302 $this->_storage->network_install_blog_id = $first_blog_id;
14303 }
14304
14305 if ( count( $installs ) === count( $address_to_blog_map ) ) {
14306 // Super-admin opted-in for all sites in the network.
14307 $this->_storage->is_network_connected = true;
14308 }
14309
14310 $this->_store_licenses( false );
14311
14312 self::$_accounts->store();
14313
14314 // Don't sync the installs data on network upgrade
14315 if ( ! $this->network_upgrade_mode_completed() ) {
14316 $this->send_installs_update();
14317 }
14318
14319 // Switch install context back to the first install.
14320 $this->_site = $first_install;
14321
14322 $current_blog = get_current_blog_id();
14323
14324 foreach ( $blog_2_install_map as $blog_id => $install ) {
14325 $this->switch_to_blog( $blog_id );
14326
14327 $this->do_action( 'after_account_connection', $user, $install );
14328 }
14329
14330 $this->switch_to_blog( $current_blog );
14331
14332 $this->do_action( 'after_network_account_connection', $user, $blog_2_install_map );
14333 }
14334
14335 if ( is_numeric( $first_install->license_id ) ) {
14336 $this->_license = $this->_get_license_by_id( $first_install->license_id );
14337 }
14338
14339 $this->_admin_notices->remove_sticky( 'connect_account' );
14340
14341 if ( $this->is_pending_activation() || ! $this->has_settings_menu() ) {
14342 // Remove pending activation sticky notice (if still exist).
14343 $this->_admin_notices->remove_sticky( 'activation_pending' );
14344
14345 // Remove plugin from pending activation mode.
14346 unset( $this->_storage->is_pending_activation );
14347
14348 if ( ! $this->is_paying_or_trial() ) {
14349 $this->_admin_notices->add_sticky(
14350 sprintf( $this->get_text_inline( '%s activation was successfully completed.', 'plugin-x-activation-message' ), '<b>' . $this->get_plugin_name() . '</b>' ),
14351 'activation_complete'
14352 );
14353 }
14354 }
14355
14356 if ( $this->is_paying_or_trial() ) {
14357 if ( ! $this->is_premium() ||
14358 ! $this->has_premium_version() ||
14359 ! $this->has_settings_menu()
14360 ) {
14361 if ( $this->is_paying() ) {
14362 $this->_admin_notices->add_sticky(
14363 sprintf(
14364 $this->get_text_inline( 'Your account was successfully activated with the %s plan.', 'activation-with-plan-x-message' ),
14365 $this->get_plan_title()
14366 ) . $this->get_complete_upgrade_instructions(),
14367 'plan_upgraded',
14368 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
14369 );
14370 } else {
14371 $trial_plan = $this->get_trial_plan();
14372
14373 $this->_admin_notices->add_sticky(
14374 sprintf(
14375 $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ),
14376 '<i>' . $this->get_plugin_name() . '</i>'
14377 ) . $this->get_complete_upgrade_instructions( $trial_plan->title ),
14378 'trial_started',
14379 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
14380 );
14381 }
14382 }
14383
14384 $this->_admin_notices->remove_sticky( array(
14385 'trial_promotion',
14386 ) );
14387 }
14388
14389 $plugin_id = fs_request_get( 'plugin_id', false );
14390
14391 // Store activation time ONLY for plugins & themes (not add-ons).
14392 if ( ! is_numeric( $plugin_id ) || ( $plugin_id == $this->_plugin->id ) ) {
14393 if ( empty( $this->_storage->activation_timestamp ) ) {
14394 $this->_storage->activation_timestamp = WP_FS__SCRIPT_START_TIME;
14395 }
14396 }
14397
14398 $next_page = '';
14399
14400 $extra = array();
14401 if ( $auto_install ) {
14402 $extra['auto_install'] = 'true';
14403 }
14404
14405 if ( is_numeric( $plugin_id ) ) {
14406 /**
14407 * @author Leo Fajardo (@leorw)
14408 * @since 1.2.1.6
14409 *
14410 * Also sync the license after an anonymous user subscribes.
14411 */
14412 if ( $this->is_anonymous() || $plugin_id != $this->_plugin->id ) {
14413 // Add-on was installed - sync license right after install.
14414 $next_page = $this->_get_sync_license_url( $plugin_id, true, $extra );
14415 }
14416 } else {
14417 /**
14418 * @author Vova Feldman (@svovaf)
14419 * @since 1.1.9 If site installed with a valid license, sync license.
14420 */
14421 if ( $this->is_paying() ) {
14422 $this->_sync_plugin_license(
14423 true,
14424 // Installs data is already synced in the beginning of this method directly or via _set_account().
14425 false
14426 );
14427 }
14428
14429 // Reload the page with the keys.
14430 $next_page = $this->is_anonymous() ?
14431 // If user previously skipped, redirect to account page.
14432 $this->get_account_url( false, $extra ) :
14433 $this->get_after_activation_url( 'after_connect_url', array(), $is_network_level_opt_in );
14434 }
14435
14436 if ( ! empty( $next_page ) && $redirect ) {
14437 fs_redirect( $next_page );
14438 }
14439
14440 return $next_page;
14441 }
14442
14443 /**
14444 * Install plugin with new user information after approval.
14445 *
14446 * @author Vova Feldman (@svovaf)
14447 * @since 1.0.7
14448 */
14449 function _install_with_new_user() {
14450 $this->_logger->entrance();
14451
14452 if ( $this->is_registered() ) {
14453 return;
14454 }
14455
14456 if ( ( $this->is_plugin() && fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) ||
14457 // @todo This logic should be improved because it's executed on every load of a theme.
14458 $this->is_theme()
14459 ) {
14460 // check_admin_referer( $this->_slug . '_activate_new' );
14461
14462 if ( fs_request_has( 'user_secret_key' ) ) {
14463 if ( fs_is_network_admin() && isset( $this->_storage->pending_sites_info ) ) {
14464 $pending_sites_info = $this->_storage->pending_sites_info;
14465
14466 $this->install_many_pending_with_user(
14467 fs_request_get( 'user_id' ),
14468 fs_request_get( 'user_public_key' ),
14469 fs_request_get( 'user_secret_key' ),
14470 fs_request_get_bool( 'is_marketing_allowed', null ),
14471 $pending_sites_info['blog_ids'],
14472 $pending_sites_info['license_key'],
14473 $pending_sites_info['trial_plan_id']
14474 );
14475 } else {
14476 $this->install_with_new_user(
14477 fs_request_get( 'user_id' ),
14478 fs_request_get( 'user_public_key' ),
14479 fs_request_get( 'user_secret_key' ),
14480 fs_request_get_bool( 'is_marketing_allowed', null ),
14481 fs_request_get( 'install_id' ),
14482 fs_request_get( 'install_public_key' ),
14483 fs_request_get( 'install_secret_key' ),
14484 true,
14485 fs_request_get_bool( 'auto_install' )
14486 );
14487 }
14488 } else if ( fs_request_has( 'pending_activation' ) ) {
14489 $this->set_pending_confirmation( fs_request_get( 'user_email' ), true );
14490 }
14491 }
14492 }
14493
14494 /**
14495 * @author Vova Feldman (@svovaf)
14496 * @since 2.0.0
14497 *
14498 * @param number $id
14499 * @param string $public_key
14500 * @param string $secret_key
14501 *
14502 * @return \FS_User
14503 */
14504 private function setup_user( $id, $public_key, $secret_key ) {
14505 $user = self::_get_user_by_id( $id );
14506
14507 if ( is_object( $user ) ) {
14508 $this->_user = $user;
14509 } else {
14510 $user = new FS_User();
14511 $user->id = $id;
14512 $user->public_key = $public_key;
14513 $user->secret_key = $secret_key;
14514
14515 $this->_user = $user;
14516 $user_result = $this->get_api_user_scope()->get();
14517 $user = new FS_User( $user_result );
14518
14519 $this->_user = $user;
14520 $this->_store_user();
14521 }
14522
14523 return $user;
14524 }
14525
14526 /**
14527 * Install plugin with new user.
14528 *
14529 * @author Vova Feldman (@svovaf)
14530 * @since 1.1.7.4
14531 *
14532 * @param number $user_id
14533 * @param string $user_public_key
14534 * @param string $user_secret_key
14535 * @param bool|null $is_marketing_allowed
14536 * @param number $install_id
14537 * @param string $install_public_key
14538 * @param string $install_secret_key
14539 * @param bool $redirect
14540 * @param bool $auto_install Since 1.2.1.7 If `true` and setting up an account with a valid license, will
14541 * redirect (or return a URL) to the account page with a special parameter to
14542 * trigger the auto installation processes.
14543 *
14544 * @return string If redirect is `false`, returns the next page the user should be redirected to.
14545 */
14546 private function install_with_new_user(
14547 $user_id,
14548 $user_public_key,
14549 $user_secret_key,
14550 $is_marketing_allowed,
14551 $install_id,
14552 $install_public_key,
14553 $install_secret_key,
14554 $redirect = true,
14555 $auto_install = false
14556 ) {
14557 /**
14558 * This method is also executed after opting in with a license key since the
14559 * license can be potentially associated with a different owner.
14560 *
14561 * @since 2.0.0
14562 */
14563 $user = self::_get_user_by_id( $user_id );
14564
14565 if ( ! is_object( $user ) ) {
14566 $user = new FS_User();
14567 $user->id = $user_id;
14568 $user->public_key = $user_public_key;
14569 $user->secret_key = $user_secret_key;
14570
14571 $this->_user = $user;
14572 $user_result = $this->get_api_user_scope()->get();
14573 $user = new FS_User( $user_result );
14574 }
14575
14576 $this->_user = $user;
14577
14578 $site = new FS_Site();
14579 $site->id = $install_id;
14580 $site->public_key = $install_public_key;
14581 $site->secret_key = $install_secret_key;
14582
14583 $this->_site = $site;
14584 $site_result = $this->get_api_site_scope()->get();
14585 $site = new FS_Site( $site_result );
14586 $this->_site = $site;
14587
14588 if ( ! is_null( $is_marketing_allowed ) ) {
14589 $this->disable_opt_in_notice_and_lock_user();
14590 }
14591
14592 return $this->setup_account(
14593 $this->_user,
14594 $this->_site,
14595 $redirect,
14596 $auto_install
14597 );
14598 }
14599
14600 /**
14601 * Install plugin with user.
14602 *
14603 * @author Leo Fajardo (@leorw)
14604 * @since 2.0.0
14605 *
14606 * @param number $user_id
14607 * @param string $user_public_key
14608 * @param string $user_secret_key
14609 * @param bool|null $is_marketing_allowed
14610 * @param array $site_ids
14611 * @param bool $license_key
14612 * @param bool $trial_plan_id
14613 * @param bool $redirect
14614 *
14615 * @return string If redirect is `false`, returns the next page the user should be redirected to.
14616 */
14617 private function install_many_pending_with_user(
14618 $user_id,
14619 $user_public_key,
14620 $user_secret_key,
14621 $is_marketing_allowed,
14622 $site_ids,
14623 $license_key = false,
14624 $trial_plan_id = false,
14625 $redirect = true
14626 ) {
14627 $user = $this->setup_user( $user_id, $user_public_key, $user_secret_key );
14628
14629 if ( ! is_null( $is_marketing_allowed ) ) {
14630 $this->disable_opt_in_notice_and_lock_user();
14631 }
14632
14633 $sites = array();
14634 foreach ( $site_ids as $site_id ) {
14635 $sites[] = $this->get_site_info( array( 'blog_id' => $site_id ) );
14636 }
14637
14638 $this->install_with_user( $user, $license_key, $trial_plan_id, $redirect, true, $sites );
14639 }
14640
14641 /**
14642 * Multi-site install with a new user.
14643 *
14644 * @author Vova Feldman (@svovaf)
14645 * @since 2.0.0
14646 *
14647 * @param number $user_id
14648 * @param string $user_public_key
14649 * @param string $user_secret_key
14650 * @param bool|null $is_marketing_allowed
14651 * @param object[] $installs
14652 * @param bool $redirect
14653 * @param bool $auto_install Since 1.2.1.7 If `true` and setting up an account with a valid license, will
14654 * redirect (or return a URL) to the account page with a special parameter to
14655 * trigger the auto installation processes.
14656 *
14657 * @return string If redirect is `false`, returns the next page the user should be redirected to.
14658 */
14659 private function install_many_with_new_user(
14660 $user_id,
14661 $user_public_key,
14662 $user_secret_key,
14663 $is_marketing_allowed,
14664 array $installs,
14665 $redirect = true,
14666 $auto_install = false
14667 ) {
14668 $this->setup_user( $user_id, $user_public_key, $user_secret_key );
14669
14670 if ( ! is_null( $is_marketing_allowed ) ) {
14671 $this->disable_opt_in_notice_and_lock_user();
14672 }
14673
14674 $install_ids = array();
14675
14676 foreach ( $installs as $install ) {
14677 $install_ids[] = $install->id;
14678 }
14679
14680 $left = count( $install_ids );
14681 $offset = 0;
14682
14683 $installs = array();
14684 while ( $left > 0 ) {
14685 $result = $this->get_api_user_scope()->get( "/plugins/{$this->_module_id}/installs.json?ids=" . implode( ',', array_slice( $install_ids, $offset, 25 ) ) );
14686
14687 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
14688 // @todo Handle API error.
14689 }
14690
14691 $installs = array_merge( $installs, $result->installs );
14692
14693 $left -= 25;
14694 }
14695
14696 foreach ( $installs as &$install ) {
14697 $install = new FS_Site( $install );
14698 }
14699
14700 return $this->setup_network_account(
14701 $this->_user,
14702 $installs,
14703 $redirect,
14704 $auto_install
14705 );
14706 }
14707
14708 /**
14709 * @author Vova Feldman (@svovaf)
14710 * @since 1.1.7.4
14711 *
14712 * @param string|bool $email
14713 * @param bool $redirect
14714 * @param string|bool $license_key Since 1.2.1.5
14715 * @param bool $is_pending_trial Since 1.2.1.5
14716 *
14717 * @return string Since 1.2.1.5 if $redirect is `false`, return the pending activation page.
14718 */
14719 private function set_pending_confirmation(
14720 $email = false,
14721 $redirect = true,
14722 $license_key = false,
14723 $is_pending_trial = false
14724 ) {
14725 if ( $this->_ignore_pending_mode ) {
14726 /**
14727 * If explicitly asked to ignore pending mode, set to anonymous mode
14728 * if require confirmation before finalizing the opt-in.
14729 *
14730 * @author Vova Feldman
14731 * @since 1.2.1.6
14732 */
14733 $this->skip_connection( null, fs_is_network_admin() );
14734 } else {
14735 // Install must be activated via email since
14736 // user with the same email already exist.
14737 $this->_storage->is_pending_activation = true;
14738 $this->_add_pending_activation_notice( $email, $is_pending_trial );
14739 }
14740
14741 if ( ! empty( $license_key ) ) {
14742 $this->_storage->pending_license_key = $license_key;
14743 }
14744
14745 // Remove the opt-in sticky notice.
14746 $this->_admin_notices->remove_sticky( array(
14747 'connect_account',
14748 'trial_promotion',
14749 ) );
14750
14751 $next_page = $this->get_after_activation_url( 'after_pending_connect_url' );
14752
14753 // Reload the page with with pending activation message.
14754 if ( $redirect ) {
14755 fs_redirect( $next_page );
14756 }
14757
14758 return $next_page;
14759 }
14760
14761 /**
14762 * Install plugin with current logged WP user info.
14763 *
14764 * @author Vova Feldman (@svovaf)
14765 * @since 1.0.7
14766 */
14767 function _install_with_current_user() {
14768 $this->_logger->entrance();
14769
14770 if ( $this->is_registered() ) {
14771 return;
14772 }
14773
14774 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) && fs_request_is_post() ) {
14775 // check_admin_referer( 'activate_existing_' . $this->_plugin->public_key );
14776
14777 /**
14778 * @author Vova Feldman (@svovaf)
14779 * @since 1.1.9 Add license key if given.
14780 */
14781 $license_key = fs_request_get( 'license_secret_key' );
14782
14783 $this->install_with_current_user( $license_key );
14784 }
14785 }
14786
14787
14788 /**
14789 * @author Vova Feldman (@svovaf)
14790 * @since 1.1.7.4
14791 *
14792 * @param string|bool $license_key
14793 * @param number|bool $trial_plan_id
14794 * @param array $sites Since 2.0.0
14795 * @param bool $redirect
14796 *
14797 * @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.
14798 */
14799 private function install_with_current_user(
14800 $license_key = false,
14801 $trial_plan_id = false,
14802 $sites = array(),
14803 $redirect = true
14804 ) {
14805 // Get current logged WP user.
14806 $current_user = self::_get_current_wp_user();
14807
14808 // Find the relevant FS user by the email.
14809 $user = self::_get_user_by_email( $current_user->user_email );
14810
14811 return $this->install_with_user( $user, $license_key, $trial_plan_id, $redirect, true, $sites );
14812 }
14813
14814 /**
14815 * @author Vova Feldman (@svovaf)
14816 * @since 2.0.0
14817 *
14818 * @param \FS_User $user
14819 * @param string|bool $license_key
14820 * @param number|bool $trial_plan_id
14821 * @param bool $redirect
14822 * @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.
14823 * @param array $sites Since 2.0.0. If not empty, should be a collection of site details for the bulk install API request.
14824 *
14825 * @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.
14826 */
14827 private function install_with_user(
14828 FS_User $user,
14829 $license_key = false,
14830 $trial_plan_id = false,
14831 $redirect = true,
14832 $setup_account = true,
14833 $sites = array()
14834 ) {
14835 // We have to set the user before getting user scope API handler.
14836 $this->_user = $user;
14837
14838 // Install the plugin.
14839 $result = $this->create_installs_with_user(
14840 $user,
14841 $license_key,
14842 $trial_plan_id,
14843 $sites,
14844 $redirect
14845 );
14846
14847 if ( ! $this->is_api_result_entity( $result ) &&
14848 ! $this->is_api_result_object( $result, 'installs' )
14849 ) {
14850 // @todo Handler potential API error of the $result
14851 }
14852
14853 if ( empty( $sites ) ) {
14854 $site = new FS_Site( $result );
14855 $this->_site = $site;
14856
14857 if ( ! $setup_account ) {
14858 $this->_store_site();
14859
14860 $this->sync_plan_if_not_exist( $site->plan_id );
14861
14862 if ( ! empty( $license_key ) && FS_Plugin_License::is_valid_id( $site->license_id ) ) {
14863 $this->sync_license_if_not_exist( $site->license_id, $license_key );
14864 }
14865
14866 $this->_admin_notices->remove_sticky( 'connect_account', false );
14867
14868 return $site;
14869 }
14870
14871 return $this->setup_account( $this->_user, $this->_site, $redirect );
14872 } else {
14873 $installs = array();
14874 foreach ( $result->installs as $install ) {
14875 $installs[] = new FS_Site( $install );
14876 }
14877
14878 return $this->setup_network_account(
14879 $user,
14880 $installs,
14881 $redirect
14882 );
14883 }
14884 }
14885
14886 /**
14887 * Initiate an API request to create a collection of installs.
14888 *
14889 * @author Vova Feldman (@svovaf)
14890 * @since 2.0.0
14891 *
14892 * @param \FS_User $user
14893 * @param bool $license_key
14894 * @param bool $trial_plan_id
14895 * @param array $sites
14896 * @param bool $redirect
14897 * @param bool $silent
14898 *
14899 * @return object|mixed
14900 */
14901 private function create_installs_with_user(
14902 FS_User $user,
14903 $license_key = false,
14904 $trial_plan_id = false,
14905 $sites = array(),
14906 $redirect = false,
14907 $silent = false
14908 ) {
14909 $extra_install_params = array(
14910 'uid' => $this->get_anonymous_id(),
14911 'is_disconnected' => false,
14912 );
14913
14914 if ( ! empty( $license_key ) ) {
14915 $extra_install_params['license_key'] = $this->apply_filters( 'license_key', $license_key );
14916 } else if ( FS_Plugin_Plan::is_valid_id( $trial_plan_id ) ) {
14917 $extra_install_params['trial_plan_id'] = $trial_plan_id;
14918 }
14919
14920 if ( ! empty( $sites ) ) {
14921 $extra_install_params['sites'] = $sites;
14922 }
14923
14924 $args = $this->get_install_data_for_api( $extra_install_params, false, false );
14925
14926 // Install the plugin.
14927 $result = $this->get_api_user_scope_by_user( $user )->call(
14928 "/plugins/{$this->get_id()}/installs.json",
14929 'post',
14930 $args
14931 );
14932
14933 if ( ! $this->is_api_result_entity( $result ) &&
14934 ! $this->is_api_result_object( $result, 'installs' )
14935 ) {
14936 if ( ! empty( $args['license_key'] ) ) {
14937 // Pass full the fully entered license key to the failure handler.
14938 $args['license_key'] = $license_key;
14939 }
14940
14941 $result = $this->apply_filters( 'after_install_failure', $result, $args );
14942
14943 if ( ! $silent ) {
14944 $this->_admin_notices->add(
14945 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
14946 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $result->error->message . '</b>',
14947 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
14948 'error'
14949 );
14950 }
14951
14952 if ( $redirect ) {
14953 /**
14954 * We set the user before getting the user scope API handler, so the user became temporarily
14955 * registered (`is_registered() = true`). Since the API returned an error and we will redirect,
14956 * we have to set the user to `null`, otherwise, the user will be redirected to the wrong
14957 * activation page based on the return value of `is_registered()`. In addition, in case the
14958 * context plugin doesn't have a settings menu and the default page is the `Plugins` page,
14959 * misleading plugin activation errors will be shown on the `Plugins` page.
14960 *
14961 * @author Leo Fajardo (@leorw)
14962 */
14963 $this->_user = null;
14964
14965 fs_redirect( $this->get_activation_url( array( 'error' => $result->error->message ) ) );
14966 }
14967 }
14968
14969 return $result;
14970 }
14971
14972 /**
14973 * Tries to activate add-on account based on parent plugin info.
14974 *
14975 * @author Vova Feldman (@svovaf)
14976 * @since 1.0.6
14977 *
14978 * @param Freemius $parent_fs
14979 */
14980 private function _activate_addon_account( Freemius $parent_fs ) {
14981 if ( $this->is_registered() ) {
14982 // Already activated.
14983 return;
14984 }
14985
14986 // Activate add-on with parent plugin credentials.
14987 $addon_install = $parent_fs->get_api_site_scope()->call(
14988 "/addons/{$this->_plugin->id}/installs.json",
14989 'post',
14990 $this->get_install_data_for_api( array(
14991 'uid' => $this->get_anonymous_id(),
14992 ), false, false )
14993 );
14994
14995 if ( isset( $addon_install->error ) ) {
14996 $this->_admin_notices->add(
14997 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
14998 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $addon_install->error->message . '</b>',
14999 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
15000 'error'
15001 );
15002
15003 return;
15004 }
15005
15006 // Get user information based on parent's plugin.
15007 $user = $parent_fs->get_user();
15008
15009 // First of all, set site and user info - otherwise we won't
15010 // be able to invoke API calls.
15011 $this->_site = new FS_Site( $addon_install );
15012 $this->_user = $user;
15013
15014 // Sync add-on plans.
15015 $this->_sync_plans();
15016
15017 // Get site's current plan.
15018 //$this->_site->plan = $this->_get_plan_by_id( $this->_site->plan->id );
15019
15020 $this->_set_account( $user, $this->_site );
15021
15022 // Sync licenses.
15023 $this->_sync_licenses();
15024
15025 // Try to activate premium license.
15026 $this->_activate_license( true );
15027 }
15028
15029 /**
15030 * Tries to activate parent account based on add-on's info.
15031 *
15032 * @author Vova Feldman (@svovaf)
15033 * @since 1.2.2.7
15034 *
15035 * @param Freemius $parent_fs
15036 */
15037 private function activate_parent_account( Freemius $parent_fs ) {
15038 if ( ! $this->is_addon() ) {
15039 // This is not an add-on.
15040 return;
15041 }
15042
15043 if ( $parent_fs->is_registered() ) {
15044 // Already activated.
15045 return;
15046 }
15047
15048 // Activate parent with add-on's user credentials.
15049 $parent_install = $this->get_api_user_scope()->call(
15050 "/plugins/{$parent_fs->_plugin->id}/installs.json",
15051 'post',
15052 $parent_fs->get_install_data_for_api( array(
15053 'uid' => $parent_fs->get_anonymous_id(),
15054 ), false, false )
15055 );
15056
15057 if ( isset( $parent_install->error ) ) {
15058 $this->_admin_notices->add(
15059 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
15060 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $parent_install->error->message . '</b>',
15061 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
15062 'error'
15063 );
15064
15065 return;
15066 }
15067
15068 $parent_fs->_admin_notices->remove_sticky( 'connect_account' );
15069
15070 if ( $parent_fs->is_pending_activation() ) {
15071 $parent_fs->_admin_notices->remove_sticky( 'activation_pending' );
15072
15073 unset( $parent_fs->_storage->is_pending_activation );
15074 }
15075
15076 // Get user information based on parent's plugin.
15077 $user = $this->get_user();
15078
15079 // First of all, set site info - otherwise we won't
15080 // be able to invoke API calls.
15081 $parent_fs->_site = new FS_Site( $parent_install );
15082 $parent_fs->_user = $user;
15083
15084 // Sync add-on plans.
15085 $parent_fs->_sync_plans();
15086
15087 $parent_fs->_set_account( $user, $parent_fs->_site );
15088 }
15089
15090 #endregion
15091
15092 #----------------------------------------------------------------------------------
15093 #region Admin Menu Items
15094 #----------------------------------------------------------------------------------
15095
15096 private $_menu_items = array();
15097
15098 /**
15099 * @author Vova Feldman (@svovaf)
15100 * @since 1.2.1.8
15101 *
15102 * @return array
15103 */
15104 function get_menu_items() {
15105 return $this->_menu_items;
15106 }
15107
15108 /**
15109 * @author Vova Feldman (@svovaf)
15110 * @since 1.0.7
15111 *
15112 * @return string
15113 */
15114 function get_menu_slug() {
15115 return $this->_menu->get_slug();
15116 }
15117
15118 /**
15119 * @author Vova Feldman (@svovaf)
15120 * @since 1.0.9
15121 */
15122 function _prepare_admin_menu() {
15123 // if ( ! $this->is_on() ) {
15124 // return;
15125 // }
15126
15127 /**
15128 * When running from a site admin with a network activated module and the connection
15129 * was NOT delegated and the user still haven't skipped or opted-in, then hide the
15130 * site level settings.
15131 *
15132 * @author Vova Feldman (@svovaf)
15133 * @since 2.0.0
15134 */
15135 $should_hide_site_admin_settings = (
15136 $this->_is_network_active &&
15137 ! fs_is_network_admin() &&
15138 ! $this->is_delegated_connection() &&
15139 ! $this->is_anonymous() &&
15140 ! $this->is_registered()
15141 );
15142
15143 if ( ( ! $this->has_api_connectivity() && ! $this->is_enable_anonymous() ) ||
15144 $should_hide_site_admin_settings
15145 ) {
15146 $this->_menu->remove_menu_item( $should_hide_site_admin_settings );
15147 } else {
15148 $this->do_action( fs_is_network_admin() ?
15149 'before_network_admin_menu_init' :
15150 'before_admin_menu_init'
15151 );
15152
15153 $this->add_menu_action();
15154
15155 $this->add_network_menu_when_missing();
15156
15157 $this->add_submenu_items();
15158 }
15159 }
15160
15161 /**
15162 * Admin dashboard menu items modifications.
15163 *
15164 * NOTE: admin_menu action executed before admin_init.
15165 *
15166 * @author Vova Feldman (@svovaf)
15167 * @since 1.0.7
15168 *
15169 */
15170 private function add_menu_action() {
15171 if ( $this->is_activation_mode() ) {
15172 if ( $this->is_plugin() || ( $this->has_settings_menu() && ! $this->is_free_wp_org_theme() ) ) {
15173 $this->override_plugin_menu_with_activation();
15174 } else {
15175 /**
15176 * Handle theme opt-in when the opt-in form shows as a dialog box in the themes page.
15177 */
15178 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) ) {
15179 add_action( 'load-themes.php', array( &$this, '_install_with_current_user' ) );
15180 } else if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ||
15181 fs_request_get_bool( 'pending_activation' )
15182 ) {
15183 add_action( 'load-themes.php', array( &$this, '_install_with_new_user' ) );
15184 }
15185 }
15186 } else {
15187 if ( ! $this->is_registered() ) {
15188 // If not registered try to install user.
15189 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) {
15190 $this->_install_with_new_user();
15191 }
15192 } else if (
15193 fs_request_is_action( 'sync_user' ) &&
15194 ( ! $this->has_settings_menu() || $this->is_free_wp_org_theme() )
15195 ) {
15196 $this->_handle_account_user_sync();
15197 }
15198 }
15199 }
15200
15201 /**
15202 * @author Vova Feldman (@svovaf)
15203 * @since 1.0.1
15204 */
15205 function _redirect_on_clicked_menu_link() {
15206 $this->_logger->entrance();
15207
15208 $page = fs_request_get('page');
15209 $page = is_string($page) ? strtolower($page) : '';
15210
15211 $this->_logger->log( 'page = ' . $page );
15212
15213 foreach ( $this->_menu_items as $priority => $items ) {
15214 foreach ( $items as $item ) {
15215 if ( isset( $item['url'] ) ) {
15216 if ( $page === $this->_menu->get_slug( strtolower( $item['menu_slug'] ) ) ) {
15217 $this->_logger->log( 'Redirecting to ' . $item['url'] );
15218
15219 fs_redirect( $item['url'] );
15220 }
15221 }
15222 }
15223 }
15224 }
15225
15226 /**
15227 * Remove plugin's all admin menu items & pages, and replace with activation page.
15228 *
15229 * @author Vova Feldman (@svovaf)
15230 * @since 1.0.1
15231 */
15232 private function override_plugin_menu_with_activation() {
15233 $this->_logger->entrance();
15234
15235 $hook = false;
15236
15237 if ( ! $this->has_settings_menu() ) {
15238 // Add the opt-in page without a menu item.
15239 $hook = FS_Admin_Menu_Manager::add_subpage(
15240 null,
15241 $this->get_plugin_name(),
15242 $this->get_plugin_name(),
15243 'manage_options',
15244 $this->_slug,
15245 array( &$this, '_connect_page_render' )
15246 );
15247 } else if ( $this->_menu->is_top_level() ) {
15248 if ( $this->_menu->is_override_exact() ) {
15249 // Make sure the current page is matching the activation page.
15250 if ( ! $this->is_matching_url( $this->get_activation_url() ) ) {
15251 return;
15252 }
15253 }
15254
15255 $hook = $this->_menu->override_menu_item( array( &$this, '_connect_page_render' ) );
15256
15257 if ( false === $hook ) {
15258 // Create new menu item just for the opt-in.
15259 $hook = FS_Admin_Menu_Manager::add_page(
15260 $this->get_plugin_name(),
15261 $this->get_plugin_name(),
15262 'manage_options',
15263 $this->_menu->get_slug(),
15264 array( &$this, '_connect_page_render' )
15265 );
15266 }
15267 } else {
15268 $menus = array( $this->_menu->get_parent_slug() );
15269
15270 if ( $this->_menu->is_override_exact() ) {
15271 // Make sure the current page is matching the activation page.
15272 if ( ! $this->is_matching_url( $this->get_activation_url() ) ) {
15273 return;
15274 }
15275 }
15276
15277 foreach ( $menus as $parent_slug ) {
15278 $hook = $this->_menu->override_submenu_action(
15279 $parent_slug,
15280 $this->_menu->get_raw_slug(),
15281 array( &$this, '_connect_page_render' )
15282 );
15283
15284 if ( false !== $hook ) {
15285 // Found plugin's submenu item.
15286 break;
15287 }
15288 }
15289 }
15290
15291 if ( $this->is_activation_page() ) {
15292 // Clean admin page from distracting content.
15293 self::_clean_admin_content_section();
15294 }
15295
15296 if ( false !== $hook ) {
15297 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) ) {
15298 $this->_install_with_current_user();
15299 } else if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) {
15300 $this->_install_with_new_user();
15301 }
15302 }
15303 }
15304
15305 /**
15306 * If a plugin was network activated and connected but don't have a network
15307 * level settings, then add an artificial menu item for the Account and other
15308 * Freemius settings.
15309 *
15310 * @author Vova Feldman (@svovaf)
15311 * @since 2.0.0
15312 */
15313 private function add_network_menu_when_missing() {
15314 $this->_logger->entrance();
15315
15316 if ( ! $this->_is_network_active ) {
15317 // Plugin wasn't activated on the network level.
15318 return;
15319 }
15320
15321 if ( ! fs_is_network_admin() ) {
15322 // The context is not the network admin.
15323 return;
15324 }
15325
15326 if ( $this->_menu->has_network_menu() ) {
15327 // Plugin already has a network level menu.
15328 return;
15329 }
15330
15331 if ( $this->is_network_activation_mode() ) {
15332 /**
15333 * Do not add during activation mode, otherwise, there will be duplicate menus while the opt-in
15334 * screen is being shown.
15335 *
15336 * @author Leo Fajardo (@leorw)
15337 */
15338 return;
15339 }
15340
15341 if ( ! WP_FS__SHOW_NETWORK_EVEN_WHEN_DELEGATED ) {
15342 if ( $this->is_network_delegated_connection() ) {
15343 // Super-admin delegated the connection to the site admins.
15344 return;
15345 }
15346 }
15347
15348 if ( ! $this->_menu->has_menu() || $this->_menu->is_top_level() ) {
15349 $this->_dynamically_added_top_level_page_hook_name = $this->_menu->add_page_and_update(
15350 $this->get_plugin_name(),
15351 $this->get_plugin_name(),
15352 'manage_options',
15353 $this->_menu->has_menu() ? $this->_menu->get_slug() : $this->_slug
15354 );
15355 } else {
15356 $this->_menu->add_subpage_and_update(
15357 $this->_menu->get_parent_slug(),
15358 $this->get_plugin_name(),
15359 $this->get_plugin_name(),
15360 'manage_options',
15361 $this->_menu->get_slug()
15362 );
15363 }
15364 }
15365
15366 /**
15367 * @author Leo Fajardo (@leorw)
15368 * @since 1.2.1
15369 *
15370 * return string
15371 */
15372 function get_top_level_menu_capability() {
15373 global $menu;
15374
15375 $top_level_menu_slug = $this->get_top_level_menu_slug();
15376
15377 foreach ( $menu as $menu_info ) {
15378 /**
15379 * The second element in the menu info array is the capability/role that has access to the menu and the
15380 * third element is the menu slug.
15381 */
15382 if ( $menu_info[2] === $top_level_menu_slug ) {
15383 return $menu_info[1];
15384 }
15385 }
15386
15387 return 'read';
15388 }
15389
15390 /**
15391 * @author Vova Feldman (@svovaf)
15392 * @since 1.0.0
15393 *
15394 * @return string
15395 */
15396 private function get_top_level_menu_slug() {
15397 return ( $this->is_addon() ?
15398 $this->get_parent_instance()->_menu->get_top_level_menu_slug() :
15399 $this->_menu->get_top_level_menu_slug() );
15400 }
15401
15402 /**
15403 * @author Vova Feldman (@svovaf)
15404 * @since 1.2.2.7
15405 *
15406 * @return string
15407 */
15408 function get_pricing_cta_label() {
15409 $label = $this->get_text_inline( 'Upgrade', 'upgrade' );
15410
15411 if ( $this->is_in_trial_promotion() &&
15412 ! $this->is_paying_or_trial()
15413 ) {
15414 // If running a trial promotion, modify the pricing to load the trial.
15415 $label = $this->get_text_inline( 'Start Trial', 'start-trial' );
15416 } else if ( $this->is_paying() ) {
15417 $label = $this->get_text_inline( 'Pricing', 'pricing' );
15418 }
15419
15420 return $label;
15421 }
15422
15423 /**
15424 * @author Vova Feldman (@svovaf)
15425 * @since 1.2.2.7
15426 *
15427 * @return bool
15428 */
15429 function is_pricing_page_visible() {
15430 return (
15431 // Has at least one paid plan.
15432 $this->has_paid_plan() &&
15433 // Didn't ask to hide the pricing page.
15434 $this->is_page_visible( 'pricing' ) &&
15435 // Don't have a valid active license or has more than one plan.
15436 ( ! $this->is_paying() || ! $this->is_single_plan() )
15437 );
15438 }
15439
15440 /**
15441 * Add default Freemius menu items.
15442 *
15443 * @author Vova Feldman (@svovaf)
15444 * @since 1.0.0
15445 * @since 1.2.2.7 Also add submenu items when running in a free .org theme so the tabs will be visible.
15446 */
15447 private function add_submenu_items() {
15448 $this->_logger->entrance();
15449
15450 $is_activation_mode = $this->is_activation_mode();
15451
15452 if ( $this->is_addon() ) {
15453 // No submenu items for add-ons.
15454 $add_submenu_items = false;
15455 } else if ( $this->is_free_wp_org_theme() && ! fs_is_network_admin() ) {
15456 // Also add submenu items when running in a free .org theme so the tabs will be visible.
15457 $add_submenu_items = true;
15458 } else if ( $is_activation_mode && ! $this->is_free_wp_org_theme() ) {
15459 $add_submenu_items = false;
15460 } else if ( fs_is_network_admin() ) {
15461 /**
15462 * Add submenu items to network level when plugin was network
15463 * activated and the super-admin did NOT delegated the connection
15464 * of all sites to site admins.
15465 */
15466 $add_submenu_items = (
15467 $this->_is_network_active &&
15468 ( WP_FS__SHOW_NETWORK_EVEN_WHEN_DELEGATED ||
15469 ! $this->is_network_delegated_connection() )
15470 );
15471 } else {
15472 $add_submenu_items = ( ! $this->_is_network_active || $this->is_delegated_connection() );
15473 }
15474
15475 if ( $add_submenu_items ) {
15476 if ( $this->has_affiliate_program() ) {
15477 // Add affiliation page.
15478 $this->add_submenu_item(
15479 $this->get_text_inline( 'Affiliation', 'affiliation' ),
15480 array( &$this, '_affiliation_page_render' ),
15481 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Affiliation', 'affiliation' ),
15482 'manage_options',
15483 'affiliation',
15484 'Freemius::_clean_admin_content_section',
15485 WP_FS__DEFAULT_PRIORITY,
15486 $this->is_submenu_item_visible( 'affiliation' )
15487 );
15488 }
15489 }
15490
15491 if ( $add_submenu_items ||
15492 ( $is_activation_mode &&
15493 $this->is_only_premium() &&
15494 $this->is_admin_page( 'account' ) &&
15495 fs_request_is_action( $this->get_unique_affix() . '_sync_license' )
15496 )
15497 ) {
15498 if ( ! WP_FS__DEMO_MODE && $this->is_registered() ) {
15499 $show_account = (
15500 $this->is_submenu_item_visible( 'account' ) &&
15501 /**
15502 * @since 1.2.2.7 Don't show the Account for free WP.org themes without any paid plans.
15503 */
15504 ( ! $this->is_free_wp_org_theme() || $this->has_paid_plan() )
15505 );
15506
15507 // Add user account page.
15508 $this->add_submenu_item(
15509 $this->get_text_inline( 'Account', 'account' ),
15510 array( &$this, '_account_page_render' ),
15511 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Account', 'account' ),
15512 'manage_options',
15513 'account',
15514 array( &$this, '_account_page_load' ),
15515 WP_FS__DEFAULT_PRIORITY,
15516 ( $add_submenu_items && $show_account )
15517 );
15518 }
15519 }
15520
15521 if ( $add_submenu_items ) {
15522 // Add contact page.
15523 $this->add_submenu_item(
15524 $this->get_text_inline( 'Contact Us', 'contact-us' ),
15525 array( &$this, '_contact_page_render' ),
15526 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Contact Us', 'contact-us' ),
15527 'manage_options',
15528 'contact',
15529 'Freemius::_clean_admin_content_section',
15530 WP_FS__DEFAULT_PRIORITY,
15531 $this->is_submenu_item_visible( 'contact' )
15532 );
15533
15534 if ( $this->has_addons() ) {
15535 $this->add_submenu_item(
15536 $this->get_text_inline( 'Add-Ons', 'add-ons' ),
15537 array( &$this, '_addons_page_render' ),
15538 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Add-Ons', 'add-ons' ),
15539 'manage_options',
15540 'addons',
15541 array( &$this, '_addons_page_load' ),
15542 WP_FS__LOWEST_PRIORITY - 1,
15543 $this->is_submenu_item_visible( 'addons' )
15544 );
15545 }
15546 }
15547
15548 if ( $add_submenu_items ||
15549 ( $is_activation_mode && $this->is_only_premium() && $this->is_admin_page( 'pricing' ) )
15550 ) {
15551 if ( ! WP_FS__DEMO_MODE ) {
15552 $show_pricing = (
15553 $this->is_submenu_item_visible( 'pricing' ) &&
15554 $this->is_pricing_page_visible()
15555 );
15556
15557 $pricing_cta_text = $this->get_pricing_cta_label();
15558 $pricing_class = 'upgrade-mode';
15559 if ( $show_pricing ) {
15560 if ( $this->is_in_trial_promotion() &&
15561 ! $this->is_paying_or_trial()
15562 ) {
15563 // If running a trial promotion, modify the pricing to load the trial.
15564 $pricing_class = 'trial-mode';
15565 } else if ( $this->is_paying() ) {
15566 $pricing_class = '';
15567 }
15568 }
15569
15570 // Add upgrade/pricing page.
15571 $this->add_submenu_item(
15572 $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' ) ),
15573 array( &$this, '_pricing_page_render' ),
15574 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_x_inline( 'Pricing', 'noun', 'pricing' ),
15575 'manage_options',
15576 'pricing',
15577 'Freemius::_clean_admin_content_section',
15578 WP_FS__LOWEST_PRIORITY,
15579 ( $add_submenu_items && $show_pricing ),
15580 $pricing_class
15581 );
15582 }
15583 }
15584
15585 if ( ! $is_activation_mode || ( true !== $this->_storage->require_license_activation ) ) {
15586 /**
15587 * Add the other menu items if there are any when not in activation mode or license activation is not
15588 * required (license activation is required for registered or anonymous users after activating the
15589 * premium version when the site is not in trial mode or there's no active valid license).
15590 *
15591 * @author Leo Fajardo (@leorw)
15592 * @since 2.2.1
15593 */
15594 if ( 0 < count( $this->_menu_items ) ) {
15595 if ( ! $this->_menu->is_top_level() ) {
15596 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
15597
15598 // Append submenu items right after the plugin's submenu item.
15599 $this->order_sub_submenu_items();
15600 } else {
15601 // Append submenu items.
15602 $this->embed_submenu_items();
15603 }
15604 }
15605 }
15606 }
15607
15608 /**
15609 * Moved the actual submenu item additions to a separated function,
15610 * in order to support sub-submenu items when the plugin's settings
15611 * only have a submenu and not top-level menu item.
15612 *
15613 * @author Vova Feldman (@svovaf)
15614 * @since 1.1.4
15615 */
15616 private function embed_submenu_items() {
15617 $item_template = $this->_menu->is_top_level() ?
15618 '<span class="fs-submenu-item %s %s %s">%s</span>' :
15619 '<span class="fs-submenu-item fs-sub %s %s %s">%s</span>';
15620
15621 $top_level_menu_capability = $this->get_top_level_menu_capability();
15622
15623 ksort( $this->_menu_items );
15624
15625 $is_first_submenu_item = true;
15626
15627 foreach ( $this->_menu_items as $priority => $items ) {
15628 foreach ( $items as $item ) {
15629 $capability = ( ! empty( $item['capability'] ) ? $item['capability'] : $top_level_menu_capability );
15630
15631 $menu_item = sprintf(
15632 $item_template,
15633 $this->get_unique_affix(),
15634 $item['menu_slug'],
15635 ! empty( $item['class'] ) ? $item['class'] : '',
15636 $item['menu_title']
15637 );
15638
15639 $top_level_menu_slug = $this->get_top_level_menu_slug();
15640 $menu_slug = $this->_menu->get_slug( $item['menu_slug'] );
15641
15642 if ( ! isset( $item['url'] ) ) {
15643 $hook = FS_Admin_Menu_Manager::add_subpage(
15644 $item['show_submenu'] ?
15645 $top_level_menu_slug :
15646 null,
15647 $item['page_title'],
15648 $menu_item,
15649 $capability,
15650 $menu_slug,
15651 $item['render_function']
15652 );
15653
15654 if ( false !== $item['before_render_function'] ) {
15655 add_action( "load-$hook", $item['before_render_function'] );
15656 }
15657 } else {
15658 FS_Admin_Menu_Manager::add_subpage(
15659 $item['show_submenu'] ?
15660 $top_level_menu_slug :
15661 null,
15662 $item['page_title'],
15663 $menu_item,
15664 $capability,
15665 $menu_slug,
15666 array( $this, '' )
15667 );
15668 }
15669
15670 if ( $item['show_submenu'] && $is_first_submenu_item ) {
15671 if ( $this->_is_network_active && ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
15672 /**
15673 * If the top-level menu has been dynamically created, remove the first submenu item that
15674 * WordPress automatically creates when there's no submenu item whose slug matches the
15675 * parent's. In the following example, the `Awesome Plugin` submenu item will be removed.
15676 *
15677 * Awesome Plugin
15678 * - Awesome Plugin <-- we want to remove this since there's no real setting page for the top-level
15679 *
15680 * @author Leo Fajardo (@leorw)
15681 */
15682 remove_submenu_page( $top_level_menu_slug, $top_level_menu_slug );
15683 }
15684
15685 $is_first_submenu_item = false;
15686 }
15687 }
15688 }
15689 }
15690
15691 /**
15692 * Re-order the submenu items so all Freemius added new submenu items
15693 * are added right after the plugin's settings submenu item.
15694 *
15695 * @author Vova Feldman (@svovaf)
15696 * @since 1.1.4
15697 */
15698 private function order_sub_submenu_items() {
15699 global $submenu;
15700
15701 $menu_slug = $this->_menu->get_top_level_menu_slug();
15702
15703 /**
15704 * Before "admin_menu" fires, WordPress will loop over the default submenus and remove pages for which the user
15705 * does not have permissions. So in case a plugin does not have top-level menu but does have submenus under any
15706 * of the default menus, only users that have the right role can access its sub-submenus (Account, Contact Us,
15707 * Support Forum, etc.) since $submenu[ $menu_slug ] will be empty if the user doesn't have permission.
15708 *
15709 * In case a plugin does not have submenus under any of the default menus but does have submenus under the menu
15710 * of another plugin, only users that have the right role can access its sub-submenus since we will use the
15711 * capability needed to access the parent menu as the capability for the submenus that we will add.
15712 */
15713 if ( empty( $submenu[ $menu_slug ] ) ) {
15714 return;
15715 }
15716
15717 $top_level_menu = &$submenu[ $menu_slug ];
15718
15719 $all_submenu_items_after = array();
15720
15721 $found_submenu_item = false;
15722
15723 foreach ( $top_level_menu as $submenu_id => $meta ) {
15724 if ( $found_submenu_item ) {
15725 // Remove all submenu items after the plugin's submenu item.
15726 $all_submenu_items_after[] = $meta;
15727 unset( $top_level_menu[ $submenu_id ] );
15728 }
15729
15730 if ( $this->_menu->get_raw_slug() === $meta[2] ) {
15731 // Found the submenu item, put all below.
15732 $found_submenu_item = true;
15733 continue;
15734 }
15735 }
15736
15737 // Embed all plugin's new submenu items.
15738 $this->embed_submenu_items();
15739
15740 // Start with specially high number to make sure it's appended.
15741 $i = max( 10000, max( array_keys( $top_level_menu ) ) + 1 );
15742 foreach ( $all_submenu_items_after as $meta ) {
15743 $top_level_menu[ $i ] = $meta;
15744 $i ++;
15745 }
15746
15747 // Sort submenu items.
15748 ksort( $top_level_menu );
15749 }
15750
15751 /**
15752 * Helper method to return the module's support forum URL.
15753 *
15754 * @author Vova Feldman (@svovaf)
15755 * @since 1.2.2.7
15756 *
15757 * @return string
15758 */
15759 function get_support_forum_url() {
15760 return $this->apply_filters( 'support_forum_url', "https://wordpress.org/support/{$this->_module_type}/{$this->_slug}" );
15761 }
15762
15763 /**
15764 * Displays the Support Forum link when enabled.
15765 *
15766 * Can be filtered like so:
15767 *
15768 * function _fs_show_support_menu( $is_visible, $menu_id ) {
15769 * if ( 'support' === $menu_id ) {
15770 * return _fs->is_registered();
15771 * }
15772 * return $is_visible;
15773 * }
15774 * _fs()->add_filter('is_submenu_visible', '_fs_show_support_menu', 10, 2);
15775 *
15776 */
15777 function _add_default_submenu_items() {
15778 if ( ! $this->is_on() ) {
15779 return;
15780 }
15781
15782 if ( ! $this->is_activation_mode() &&
15783 ( ( $this->_is_network_active && fs_is_network_admin() ) ||
15784 ( ! $this->_is_network_active && is_admin() ) )
15785 ) {
15786 $this->add_submenu_link_item(
15787 $this->apply_filters( 'support_forum_submenu', $this->get_text_inline( 'Support Forum', 'support-forum' ) ),
15788 $this->get_support_forum_url(),
15789 'wp-support-forum',
15790 null,
15791 50,
15792 $this->is_submenu_item_visible( 'support' )
15793 );
15794 }
15795 }
15796
15797 /**
15798 * @author Vova Feldman (@svovaf)
15799 * @since 1.0.1
15800 *
15801 * @param string $menu_title
15802 * @param callable $render_function
15803 * @param bool|string $page_title
15804 * @param string $capability
15805 * @param bool|string $menu_slug
15806 * @param bool|callable $before_render_function
15807 * @param int $priority
15808 * @param bool $show_submenu
15809 * @param string $class Since 1.2.1.5 can add custom classes to menu items.
15810 */
15811 function add_submenu_item(
15812 $menu_title,
15813 $render_function,
15814 $page_title = false,
15815 $capability = 'manage_options',
15816 $menu_slug = false,
15817 $before_render_function = false,
15818 $priority = WP_FS__DEFAULT_PRIORITY,
15819 $show_submenu = true,
15820 $class = ''
15821 ) {
15822 $this->_logger->entrance( 'Title = ' . $menu_title );
15823
15824 if ( $this->is_addon() ) {
15825 $parent_fs = $this->get_parent_instance();
15826
15827 if ( is_object( $parent_fs ) ) {
15828 $parent_fs->add_submenu_item(
15829 $menu_title,
15830 $render_function,
15831 $page_title,
15832 $capability,
15833 $menu_slug,
15834 $before_render_function,
15835 $priority,
15836 $show_submenu,
15837 $class
15838 );
15839
15840 return;
15841 }
15842 }
15843
15844 if ( ! isset( $this->_menu_items[ $priority ] ) ) {
15845 $this->_menu_items[ $priority ] = array();
15846 }
15847
15848 $this->_menu_items[ $priority ][] = array(
15849 'page_title' => is_string( $page_title ) ? $page_title : $menu_title,
15850 'menu_title' => $menu_title,
15851 'capability' => $capability,
15852 'menu_slug' => is_string( $menu_slug ) ? $menu_slug : strtolower( $menu_title ),
15853 'render_function' => $render_function,
15854 'before_render_function' => $before_render_function,
15855 'show_submenu' => $show_submenu,
15856 'class' => $class,
15857 );
15858 }
15859
15860 /**
15861 * @author Vova Feldman (@svovaf)
15862 * @since 1.0.1
15863 *
15864 * @param string $menu_title
15865 * @param string $url
15866 * @param bool $menu_slug
15867 * @param string $capability
15868 * @param int $priority
15869 * @param bool $show_submenu
15870 */
15871 function add_submenu_link_item(
15872 $menu_title,
15873 $url,
15874 $menu_slug = false,
15875 $capability = 'read',
15876 $priority = WP_FS__DEFAULT_PRIORITY,
15877 $show_submenu = true
15878 ) {
15879 $this->_logger->entrance( 'Title = ' . $menu_title . '; Url = ' . $url );
15880
15881 if ( $this->is_addon() ) {
15882 $parent_fs = $this->get_parent_instance();
15883
15884 if ( is_object( $parent_fs ) ) {
15885 $parent_fs->add_submenu_link_item(
15886 $menu_title,
15887 $url,
15888 $menu_slug,
15889 $capability,
15890 $priority,
15891 $show_submenu
15892 );
15893
15894 return;
15895 }
15896 }
15897
15898 if ( ! isset( $this->_menu_items[ $priority ] ) ) {
15899 $this->_menu_items[ $priority ] = array();
15900 }
15901
15902 $this->_menu_items[ $priority ][] = array(
15903 'menu_title' => $menu_title,
15904 'capability' => $capability,
15905 'menu_slug' => is_string( $menu_slug ) ? $menu_slug : strtolower( $menu_title ),
15906 'url' => $url,
15907 'page_title' => $menu_title,
15908 'render_function' => 'fs_dummy',
15909 'before_render_function' => '',
15910 'show_submenu' => $show_submenu,
15911 );
15912 }
15913
15914 #endregion ------------------------------------------------------------------
15915
15916
15917 #--------------------------------------------------------------------------------
15918 #region Actions / Hooks / Filters
15919 #--------------------------------------------------------------------------------
15920
15921 /**
15922 * @author Vova Feldman (@svovaf)
15923 * @since 1.1.7
15924 *
15925 * @param string $tag
15926 *
15927 * @return string
15928 */
15929 public function get_action_tag( $tag ) {
15930 return self::get_action_tag_static( $tag, $this->_slug, $this->is_plugin() );
15931 }
15932
15933 /**
15934 * @author Vova Feldman (@svovaf)
15935 * @since 1.2.1.6
15936 *
15937 * @param string $tag
15938 * @param string $slug
15939 * @param bool $is_plugin
15940 *
15941 * @return string
15942 */
15943 static function get_action_tag_static( $tag, $slug = '', $is_plugin = true ) {
15944 $action = "fs_{$tag}";
15945
15946 if ( ! empty( $slug ) ) {
15947 $action .= '_' . self::get_module_unique_affix( $slug, $is_plugin );
15948 }
15949
15950 return $action;
15951 }
15952
15953 /**
15954 * Returns a string that can be used to generate a unique action name,
15955 * option name, HTML element ID, or HTML element class.
15956 *
15957 * @author Leo Fajardo (@leorw)
15958 * @since 1.2.2
15959 *
15960 * @return string
15961 */
15962 public function get_unique_affix() {
15963 return self::get_module_unique_affix( $this->_slug, $this->is_plugin() );
15964 }
15965
15966 /**
15967 * Returns a string that can be used to generate a unique action name,
15968 * option name, HTML element ID, or HTML element class.
15969 *
15970 * @author Vova Feldman (@svovaf)
15971 * @since 1.2.2.5
15972 *
15973 * @param string $slug
15974 * @param bool $is_plugin
15975 *
15976 * @return string
15977 */
15978 static function get_module_unique_affix( $slug, $is_plugin = true ) {
15979 $affix = $slug;
15980
15981 if ( ! $is_plugin ) {
15982 $affix .= '-' . WP_FS__MODULE_TYPE_THEME;
15983 }
15984
15985 return $affix;
15986 }
15987
15988 /**
15989 * @author Vova Feldman (@svovaf)
15990 * @since 1.2.1
15991 * @since 1.2.2.5 The AJAX action names are based on the module ID, not like the non-AJAX actions that are
15992 * based on the slug for backward compatibility.
15993 *
15994 * @param string $tag
15995 *
15996 * @return string
15997 */
15998 function get_ajax_action( $tag ) {
15999 return self::get_ajax_action_static( $tag, $this->_module_id );
16000 }
16001
16002 /**
16003 * @author Vova Feldman (@svovaf)
16004 * @since 1.2.1.7
16005 *
16006 * @param string $tag
16007 *
16008 * @return string
16009 */
16010 function get_ajax_security( $tag ) {
16011 return wp_create_nonce( $this->get_ajax_action( $tag ) );
16012 }
16013
16014 /**
16015 * @author Vova Feldman (@svovaf)
16016 * @since 1.2.1.7
16017 *
16018 * @param string $tag
16019 */
16020 function check_ajax_referer( $tag ) {
16021 check_ajax_referer( $this->get_ajax_action( $tag ), 'security' );
16022 }
16023
16024 /**
16025 * @author Vova Feldman (@svovaf)
16026 * @since 1.2.1.6
16027 * @since 1.2.2.5 The AJAX action names are based on the module ID, not like the non-AJAX actions that are
16028 * based on the slug for backward compatibility.
16029 *
16030 * @param string $tag
16031 * @param number|null $module_id
16032 *
16033 * @return string
16034 */
16035 private static function get_ajax_action_static( $tag, $module_id = null ) {
16036 $action = "fs_{$tag}";
16037
16038 if ( ! empty( $module_id ) ) {
16039 $action .= "_{$module_id}";
16040 }
16041
16042 return $action;
16043 }
16044
16045 /**
16046 * Do action, specific for the current context plugin.
16047 *
16048 * @author Vova Feldman (@svovaf)
16049 * @since 1.0.1
16050 *
16051 * @param string $tag The name of the action to be executed.
16052 * @param mixed $arg,... Optional. Additional arguments which are passed on to the
16053 * functions hooked to the action. Default empty.
16054 *
16055 * @uses do_action()
16056 */
16057 function do_action( $tag, $arg = '' ) {
16058 $this->_logger->entrance( $tag );
16059
16060 $args = func_get_args();
16061
16062 call_user_func_array( 'do_action', array_merge(
16063 array( $this->get_action_tag( $tag ) ),
16064 array_slice( $args, 1 ) )
16065 );
16066 }
16067
16068 /**
16069 * Add action, specific for the current context plugin.
16070 *
16071 * @author Vova Feldman (@svovaf)
16072 * @since 1.0.1
16073 *
16074 * @param string $tag
16075 * @param callable $function_to_add
16076 * @param int $priority
16077 * @param int $accepted_args
16078 *
16079 * @uses add_action()
16080 */
16081 function add_action(
16082 $tag,
16083 $function_to_add,
16084 $priority = WP_FS__DEFAULT_PRIORITY,
16085 $accepted_args = 1
16086 ) {
16087 $this->_logger->entrance( $tag );
16088
16089 add_action( $this->get_action_tag( $tag ), $function_to_add, $priority, $accepted_args );
16090 }
16091
16092 /**
16093 * Add AJAX action, specific for the current context plugin.
16094 *
16095 * @author Vova Feldman (@svovaf)
16096 * @since 1.2.1
16097 *
16098 * @param string $tag
16099 * @param callable $function_to_add
16100 * @param int $priority
16101 *
16102 * @uses add_action()
16103 *
16104 * @return bool True if action added, false if no need to add the action since the AJAX call isn't matching.
16105 */
16106 function add_ajax_action(
16107 $tag,
16108 $function_to_add,
16109 $priority = WP_FS__DEFAULT_PRIORITY
16110 ) {
16111 $this->_logger->entrance( $tag );
16112
16113 return self::add_ajax_action_static(
16114 $tag,
16115 $function_to_add,
16116 $priority,
16117 $this->_module_id
16118 );
16119 }
16120
16121 /**
16122 * Add AJAX action.
16123 *
16124 * @author Vova Feldman (@svovaf)
16125 * @since 1.2.1.6
16126 *
16127 * @param string $tag
16128 * @param callable $function_to_add
16129 * @param int $priority
16130 * @param number|null $module_id
16131 *
16132 * @return bool True if action added, false if no need to add the action since the AJAX call isn't matching.
16133 * @uses add_action()
16134 *
16135 */
16136 static function add_ajax_action_static(
16137 $tag,
16138 $function_to_add,
16139 $priority = WP_FS__DEFAULT_PRIORITY,
16140 $module_id = null
16141 ) {
16142 self::$_static_logger->entrance( $tag );
16143
16144 if ( ! self::is_ajax_action_static( $tag, $module_id ) ) {
16145 return false;
16146 }
16147
16148 add_action(
16149 'wp_ajax_' . self::get_ajax_action_static( $tag, $module_id ),
16150 $function_to_add,
16151 $priority,
16152 0
16153 );
16154
16155 self::$_static_logger->info( "$tag AJAX callback action added." );
16156
16157 return true;
16158 }
16159
16160 /**
16161 * Send a JSON response back to an Ajax request.
16162 *
16163 * @author Vova Feldman (@svovaf)
16164 * @since 1.2.1.5
16165 *
16166 * @param mixed $response
16167 */
16168 static function shoot_ajax_response( $response ) {
16169 wp_send_json( $response );
16170 }
16171
16172 /**
16173 * Send a JSON response back to an Ajax request, indicating success.
16174 *
16175 * @author Vova Feldman (@svovaf)
16176 * @since 1.2.1.5
16177 *
16178 * @param mixed $data Data to encode as JSON, then print and exit.
16179 */
16180 static function shoot_ajax_success( $data = null ) {
16181 wp_send_json_success( $data );
16182 }
16183
16184 /**
16185 * Send a JSON response back to an Ajax request, indicating failure.
16186 *
16187 * @author Vova Feldman (@svovaf)
16188 * @since 1.2.1.5
16189 *
16190 * @param mixed $error Optional error message.
16191 */
16192 static function shoot_ajax_failure( $error = '' ) {
16193 $result = array( 'success' => false );
16194 if ( ! empty( $error ) ) {
16195 $result['error'] = $error;
16196 }
16197
16198 wp_send_json( $result );
16199 }
16200
16201 /**
16202 * Apply filter, specific for the current context plugin.
16203 *
16204 * @author Vova Feldman (@svovaf)
16205 * @since 1.0.9
16206 *
16207 * @param string $tag The name of the filter hook.
16208 * @param mixed $value The value on which the filters hooked to `$tag` are applied on.
16209 *
16210 * @return mixed The filtered value after all hooked functions are applied to it.
16211 *
16212 * @uses apply_filters()
16213 */
16214 function apply_filters( $tag, $value ) {
16215 $this->_logger->entrance( $tag );
16216
16217 $args = func_get_args();
16218 array_unshift( $args, $this->get_unique_affix() );
16219
16220 return call_user_func_array( 'fs_apply_filter', $args );
16221 }
16222
16223 /**
16224 * Add filter, specific for the current context plugin.
16225 *
16226 * @author Vova Feldman (@svovaf)
16227 * @since 1.0.9
16228 *
16229 * @param string $tag
16230 * @param callable $function_to_add
16231 * @param int $priority
16232 * @param int $accepted_args
16233 *
16234 * @uses add_filter()
16235 */
16236 function add_filter( $tag, $function_to_add, $priority = WP_FS__DEFAULT_PRIORITY, $accepted_args = 1 ) {
16237 $this->_logger->entrance( $tag );
16238
16239 add_filter( $this->get_action_tag( $tag ), $function_to_add, $priority, $accepted_args );
16240 }
16241
16242 /**
16243 * Check if has filter.
16244 *
16245 * @author Vova Feldman (@svovaf)
16246 * @since 1.1.4
16247 *
16248 * @param string $tag
16249 * @param callable|bool $function_to_check Optional. The callback to check for. Default false.
16250 *
16251 * @return false|int
16252 *
16253 * @uses has_filter()
16254 */
16255 function has_filter( $tag, $function_to_check = false ) {
16256 $this->_logger->entrance( $tag );
16257
16258 return has_filter( $this->get_action_tag( $tag ), $function_to_check );
16259 }
16260
16261 #endregion
16262
16263 /**
16264 * Override default i18n text phrases.
16265 *
16266 * @author Vova Feldman (@svovaf)
16267 * @since 1.1.6
16268 *
16269 * @param string[] string $key_value
16270 *
16271 * @uses fs_override_i18n()
16272 */
16273 function override_i18n( $key_value ) {
16274 fs_override_i18n( $key_value, $this->_slug );
16275 }
16276
16277 /* Account Page
16278 ------------------------------------------------------------------------------------------------------------------*/
16279 /**
16280 * Update site information.
16281 *
16282 * @author Vova Feldman (@svovaf)
16283 * @since 1.0.1
16284 *
16285 * @param bool $store Flush to Database if true.
16286 * @param null|int $network_level_or_blog_id Since 2.0.0
16287 * @param \FS_Site $site Since 2.0.0
16288 */
16289 private function _store_site( $store = true, $network_level_or_blog_id = null, FS_Site $site = null ) {
16290 $this->_logger->entrance();
16291
16292 if ( empty( $this->_site->id ) ) {
16293 $this->_logger->error( "Empty install ID, can't store site." );
16294
16295 return;
16296 }
16297
16298 $site_clone = is_object( $site ) ? $site : $this->_site;
16299 $encrypted_site = clone $site_clone;
16300
16301 $sites = self::get_all_sites( $this->_module_type, $network_level_or_blog_id );
16302
16303 $prev_stored_user_id = $this->_storage->get( 'prev_user_id', false, $network_level_or_blog_id );
16304
16305 if ( empty( $prev_stored_user_id ) &&
16306 $this->_user->id != $this->_site->user_id
16307 ) {
16308 /**
16309 * Store the current user ID as the previous user ID so that the previous user can be used
16310 * as the install's owner while the new owner's details are not yet available.
16311 *
16312 * This will be executed only in the `replica` site. For example, there are 2 sites, namely `original`
16313 * and `replica`, then an ownership change was initiated and completed in the `original`, the `replica`
16314 * will be using the previous user until it is updated again (e.g.: until the next clone of `original`
16315 * into `replica`.
16316 *
16317 * @author Leo Fajardo (@leorw)
16318 */
16319 $this->_storage->store( 'prev_user_id', $sites[ $this->_slug ]->user_id, $network_level_or_blog_id );
16320 }
16321
16322 $sites[ $this->_slug ] = $encrypted_site;
16323
16324 $this->set_account_option( 'sites', $sites, $store, $network_level_or_blog_id );
16325 }
16326
16327 /**
16328 * Update plugin's plans information.
16329 *
16330 * @author Vova Feldman (@svovaf)
16331 * @since 1.0.2
16332 *
16333 * @param bool $store Flush to Database if true.
16334 */
16335 private function _store_plans( $store = true ) {
16336 $this->_logger->entrance();
16337
16338 $plans = self::get_all_plans( $this->_module_type );
16339
16340 // Copy plans.
16341 $encrypted_plans = array();
16342 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
16343 $encrypted_plans[] = self::_encrypt_entity( $this->_plans[ $i ] );
16344 }
16345
16346 $plans[ $this->_slug ] = $encrypted_plans;
16347
16348 $this->set_account_option( 'plans', $plans, $store );
16349 }
16350
16351 /**
16352 * Update user's plugin licenses.
16353 *
16354 * @author Vova Feldman (@svovaf)
16355 * @since 1.0.5
16356 *
16357 * @param bool $store
16358 * @param number|bool $module_id
16359 * @param FS_Plugin_License[] $licenses
16360 */
16361 private function _store_licenses( $store = true, $module_id = false, $licenses = array() ) {
16362 $this->_logger->entrance();
16363
16364 $all_licenses = self::get_all_licenses();
16365
16366 if ( ! FS_Plugin::is_valid_id( $module_id ) ) {
16367 $module_id = $this->_module_id;
16368
16369 $user_licenses = is_array( $this->_licenses ) ?
16370 $this->_licenses :
16371 array();
16372
16373 if ( empty( $user_licenses ) ) {
16374 // If the context user doesn't have any license, don't update the licenses collection.
16375 return;
16376 }
16377
16378 $new_user_licenses_map = array();
16379 foreach ( $user_licenses as $user_license ) {
16380 $new_user_licenses_map[ $user_license->id ] = $user_license;
16381 }
16382
16383 self::store_user_id_license_ids_map( array_keys( $new_user_licenses_map ), $this->_module_id, $this->_user->id );
16384
16385 // Update user licenses.
16386 $licenses_to_update_count = count( $new_user_licenses_map );
16387 foreach ( $all_licenses[ $module_id ] as $key => $license ) {
16388 if ( 0 === $licenses_to_update_count ) {
16389 break;
16390 }
16391
16392 if ( isset( $new_user_licenses_map[ $license->id ] ) ) {
16393 // Update license.
16394 $all_licenses[ $module_id ][ $key ] = $new_user_licenses_map[ $license->id ];
16395 unset( $new_user_licenses_map[ $license->id ] );
16396
16397 $licenses_to_update_count --;
16398 }
16399 }
16400
16401 if ( ! empty( $new_user_licenses_map ) ) {
16402 // Add new licenses.
16403 $all_licenses[ $module_id ] = array_merge( array_values( $new_user_licenses_map ), $all_licenses[ $module_id ] );
16404 }
16405
16406 $licenses = $all_licenses[ $module_id ];
16407 }
16408
16409 if ( ! isset( $all_licenses[ $module_id ] ) ) {
16410 $all_licenses[ $module_id ] = array();
16411 }
16412
16413 $all_licenses[ $module_id ] = $licenses;
16414
16415 self::$_accounts->set_option( 'all_licenses', $all_licenses, $store );
16416 }
16417
16418 /**
16419 * Update user information.
16420 *
16421 * @author Vova Feldman (@svovaf)
16422 * @since 1.0.1
16423 *
16424 * @param bool $store Flush to Database if true.
16425 */
16426 private function _store_user( $store = true ) {
16427 $this->_logger->entrance();
16428
16429 if ( empty( $this->_user->id ) ) {
16430 $this->_logger->error( "Empty user ID, can't store user." );
16431
16432 return;
16433 }
16434
16435 $users = self::get_all_users();
16436 $users[ $this->_user->id ] = $this->_user;
16437 self::$_accounts->set_option( 'users', $users, $store );
16438 }
16439
16440 /**
16441 * Update new updates information.
16442 *
16443 * @author Vova Feldman (@svovaf)
16444 * @since 1.0.4
16445 *
16446 * @param FS_Plugin_Tag|null $update
16447 * @param bool $store Flush to Database if true.
16448 * @param bool|number $plugin_id
16449 */
16450 private function _store_update( $update, $store = true, $plugin_id = false ) {
16451 $this->_logger->entrance();
16452
16453 if ( $update instanceof FS_Plugin_Tag ) {
16454 $update->updated = time();
16455 }
16456
16457 if ( ! is_numeric( $plugin_id ) ) {
16458 $plugin_id = $this->_plugin->id;
16459 }
16460
16461 $updates = self::get_all_updates();
16462 $updates[ $plugin_id ] = $update;
16463 self::$_accounts->set_option( 'updates', $updates, $store );
16464 }
16465
16466 /**
16467 * Update new updates information.
16468 *
16469 * @author Vova Feldman (@svovaf)
16470 * @since 1.0.6
16471 *
16472 * @param FS_Plugin[] $plugin_addons
16473 * @param bool $store Flush to Database if true.
16474 */
16475 private function _store_addons( $plugin_addons, $store = true ) {
16476 $this->_logger->entrance();
16477
16478 $addons = self::get_all_addons();
16479 $addons[ $this->_plugin->id ] = $plugin_addons;
16480 self::$_accounts->set_option( 'addons', $addons, $store );
16481 }
16482
16483 /**
16484 * Delete plugin's associated add-ons.
16485 *
16486 * @author Vova Feldman (@svovaf)
16487 * @since 1.0.8
16488 *
16489 * @param bool $store
16490 *
16491 * @return bool
16492 */
16493 private function _delete_account_addons( $store = true ) {
16494 $all_addons = self::get_all_account_addons();
16495
16496 if ( ! isset( $all_addons[ $this->_plugin->id ] ) ) {
16497 return false;
16498 }
16499
16500 unset( $all_addons[ $this->_plugin->id ] );
16501
16502 self::$_accounts->set_option( 'account_addons', $all_addons, $store );
16503
16504 return true;
16505 }
16506
16507 /**
16508 * Update account add-ons list.
16509 *
16510 * @author Vova Feldman (@svovaf)
16511 * @since 1.0.6
16512 *
16513 * @param FS_Plugin[] $addons
16514 * @param bool $store Flush to Database if true.
16515 */
16516 private function _store_account_addons( $addons, $store = true ) {
16517 $this->_logger->entrance();
16518
16519 $all_addons = self::get_all_account_addons();
16520 $all_addons[ $this->_plugin->id ] = $addons;
16521 self::$_accounts->set_option( 'account_addons', $all_addons, $store );
16522 }
16523
16524 /**
16525 * Store account params in the Database.
16526 *
16527 * @author Vova Feldman (@svovaf)
16528 * @since 1.0.1
16529 *
16530 * @param null|int $blog_id Since 2.0.0
16531 */
16532 private function _store_account( $blog_id = null ) {
16533 $this->_logger->entrance();
16534
16535 $this->_store_site( false, $blog_id );
16536 $this->_store_user( false );
16537 $this->_store_plans( false );
16538 $this->_store_licenses( false );
16539
16540 self::$_accounts->store( $blog_id );
16541 }
16542
16543 /**
16544 * Sync user's information.
16545 *
16546 * @author Vova Feldman (@svovaf)
16547 * @since 1.0.3
16548 * @uses FS_Api
16549 */
16550 private function _handle_account_user_sync() {
16551 $this->_logger->entrance();
16552
16553 $api = $this->get_api_user_scope();
16554
16555 // Get user's information.
16556 $user = $api->get( '/', true );
16557
16558 if ( isset( $user->id ) ) {
16559 $this->_user->first = $user->first;
16560 $this->_user->last = $user->last;
16561 $this->_user->email = $user->email;
16562
16563 $is_menu_item_account_visible = $this->is_submenu_item_visible( 'account' );
16564
16565 if ( $user->is_verified &&
16566 ( ! isset( $this->_user->is_verified ) || false === $this->_user->is_verified )
16567 ) {
16568 $this->_user->is_verified = true;
16569
16570 $this->do_action( 'account_email_verified', $user->email );
16571
16572 $this->_admin_notices->add(
16573 $this->get_text_inline( 'Your email has been successfully verified - you are AWESOME!', 'email-verified-message' ),
16574 $this->get_text_x_inline( 'Right on', 'a positive response', 'right-on' ) . '!',
16575 'success',
16576 // Make admin sticky if account menu item is invisible,
16577 // since the page will be auto redirected to the plugin's
16578 // main settings page, and the non-sticky message
16579 // will disappear.
16580 ! $is_menu_item_account_visible,
16581 'email_verified'
16582 );
16583 }
16584
16585 // Flush user details to DB.
16586 $this->_store_user();
16587
16588 $this->do_action( 'after_account_user_sync', $user );
16589
16590 /**
16591 * If account menu item is hidden, redirect to plugin's main settings page.
16592 *
16593 * @author Vova Feldman (@svovaf)
16594 * @since 1.1.6
16595 *
16596 * @link https://github.com/Freemius/wordpress-sdk/issues/6
16597 */
16598 if ( ! $is_menu_item_account_visible ) {
16599 fs_redirect( $this->_get_admin_page_url() );
16600 }
16601 }
16602 }
16603
16604 /**
16605 * @author Vova Feldman (@svovaf)
16606 * @since 1.0.9
16607 * @uses FS_Api
16608 *
16609 * @param number|bool $license_id
16610 *
16611 * @return FS_Subscription|object|bool
16612 */
16613 private function _fetch_site_license_subscription( $license_id = false ) {
16614 $this->_logger->entrance();
16615 $api = $this->get_api_site_scope();
16616
16617 if ( ! is_numeric( $license_id ) ) {
16618 $license_id = $this->_license->id;
16619 }
16620
16621 $result = $api->get( "/licenses/{$license_id}/subscriptions.json", true );
16622
16623 return ! isset( $result->error ) ?
16624 ( ( is_array( $result->subscriptions ) && 0 < count( $result->subscriptions ) ) ?
16625 new FS_Subscription( $result->subscriptions[0] ) :
16626 false
16627 ) :
16628 $result;
16629 }
16630
16631 /**
16632 * @author Vova Feldman (@svovaf)
16633 * @since 1.0.4
16634 * @uses FS_Api
16635 *
16636 * @param number|bool $plan_id
16637 *
16638 * @return FS_Plugin_Plan|object
16639 */
16640 private function _fetch_site_plan( $plan_id = false ) {
16641 $this->_logger->entrance();
16642 $api = $this->get_api_site_scope();
16643
16644 if ( ! is_numeric( $plan_id ) ) {
16645 $plan_id = $this->_site->plan_id;
16646 }
16647
16648 $plan = $api->get( "/plans/{$plan_id}.json", true );
16649
16650 return ! isset( $plan->error ) ? new FS_Plugin_Plan( $plan ) : $plan;
16651 }
16652
16653 /**
16654 * @author Vova Feldman (@svovaf)
16655 * @since 1.0.5
16656 * @uses FS_Api
16657 *
16658 * @return FS_Plugin_Plan[]|object
16659 */
16660 private function _fetch_plugin_plans() {
16661 $this->_logger->entrance();
16662 $api = $this->get_current_or_network_user_api_scope();
16663
16664 /**
16665 * @since 1.2.3 When running in DEV mode, retrieve pending plans as well.
16666 */
16667 $result = $api->get( $this->add_show_pending( "/plugins/{$this->_module_id}/plans.json" ), true );
16668
16669 if ( $this->is_api_result_object( $result, 'plans' ) && is_array( $result->plans ) ) {
16670 for ( $i = 0, $len = count( $result->plans ); $i < $len; $i ++ ) {
16671 $result->plans[ $i ] = new FS_Plugin_Plan( $result->plans[ $i ] );
16672 }
16673
16674 $result = $result->plans;
16675 }
16676
16677 return $result;
16678 }
16679
16680 /**
16681 * @author Vova Feldman (@svovaf)
16682 * @since 2.0.0
16683 *
16684 * @param number $plan_id
16685 *
16686 * @return \FS_Plugin_Plan|object
16687 */
16688 private function fetch_plan_by_id( $plan_id ) {
16689 $this->_logger->entrance();
16690 $api = $this->get_current_or_network_user_api_scope();
16691
16692 $result = $api->get( "/plugins/{$this->_module_id}/plans/{$plan_id}.json", true );
16693
16694 return $this->is_api_result_entity( $result ) ?
16695 new FS_Plugin_Plan( $result ) :
16696 $result;
16697 }
16698
16699 /**
16700 * @author Vova Feldman (@svovaf)
16701 * @since 1.0.5
16702 * @uses FS_Api
16703 *
16704 * @param number|bool $plugin_id
16705 * @param number|bool $site_license_id
16706 * @param array $foreign_licenses @since 2.0.0. This is used by network-activated plugins.
16707 * @param number|null $blog_id
16708 *
16709 * @return FS_Plugin_License[]|object
16710 */
16711 private function _fetch_licenses(
16712 $plugin_id = false,
16713 $site_license_id = false,
16714 $foreign_licenses = array(),
16715 $blog_id = null
16716 ) {
16717 $this->_logger->entrance();
16718
16719 $api = $this->get_api_user_scope();
16720
16721 if ( ! is_numeric( $plugin_id ) ) {
16722 $plugin_id = $this->_plugin->id;
16723 }
16724
16725 $user_licenses_endpoint = "/plugins/{$plugin_id}/licenses.json";
16726 if ( ! empty ( $foreign_licenses ) ) {
16727 $foreign_licenses = array(
16728 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
16729 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
16730 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
16731 );
16732
16733 $user_licenses_endpoint = add_query_arg( $foreign_licenses, $user_licenses_endpoint );
16734 }
16735
16736 $result = $api->get( $user_licenses_endpoint, true );
16737
16738 $is_site_license_synced = false;
16739
16740 $api_errors = array();
16741
16742 if ( $this->is_api_result_object( $result, 'licenses' ) &&
16743 is_array( $result->licenses )
16744 ) {
16745 for ( $i = 0, $len = count( $result->licenses ); $i < $len; $i ++ ) {
16746 $result->licenses[ $i ] = new FS_Plugin_License( $result->licenses[ $i ] );
16747
16748 if ( ( ! $is_site_license_synced ) && is_numeric( $site_license_id ) ) {
16749 $is_site_license_synced = ( $site_license_id == $result->licenses[ $i ]->id );
16750 }
16751 }
16752
16753 $result = $result->licenses;
16754 } else {
16755 $api_errors[] = $result;
16756 $result = array();
16757 }
16758
16759 if ( ! $is_site_license_synced ) {
16760 if ( ! is_null( $blog_id ) ) {
16761 /**
16762 * If blog ID is not null, the request is for syncing of the license of a single site via the
16763 * network-level "Account" page.
16764 *
16765 * @author Leo Fajardo (@leorw)
16766 */
16767 $this->switch_to_blog( $blog_id );
16768 }
16769
16770 $api = $this->get_api_site_scope();
16771
16772 if ( is_numeric( $site_license_id ) ) {
16773 // Try to retrieve a foreign license that is linked to the install.
16774 $api_result = $api->call( '/licenses.json' );
16775
16776 if ( $this->is_api_result_object( $api_result, 'licenses' ) &&
16777 is_array( $api_result->licenses )
16778 ) {
16779 $licenses = $api_result->licenses;
16780
16781 if ( ! empty( $licenses ) ) {
16782 $result[] = new FS_Plugin_License( $licenses[0] );
16783 }
16784 } else {
16785 $api_errors[] = $api_result;
16786 }
16787 } else if ( is_object( $this->_license ) ) {
16788 $is_license_in_result = false;
16789 if ( ! empty( $result ) ) {
16790 foreach ( $result as $license ) {
16791 if ( $license->id == $this->_license->id ) {
16792 $is_license_in_result = true;
16793 break;
16794 }
16795 }
16796 }
16797
16798 if ( ! $is_license_in_result ) {
16799 // Fetch foreign license by ID and license key.
16800 $license = $api->get( "/licenses/{$this->_license->id}.json?license_key=" .
16801 urlencode( $this->_license->secret_key ) );
16802
16803 if ( $this->is_api_result_entity( $license ) ) {
16804 $result[] = new FS_Plugin_License( $license );
16805 } else {
16806 $api_errors[] = $license;
16807 }
16808 }
16809 }
16810
16811 if ( ! is_null( $blog_id ) ) {
16812 $this->switch_to_blog( $this->_storage->network_install_blog_id );
16813 }
16814 }
16815
16816 if ( is_array( $result ) && 0 < count( $result ) ) {
16817 // If found at least one license, return license collection even if there are errors.
16818 return $result;
16819 }
16820
16821 if ( ! empty( $api_errors ) ) {
16822 // If found any errors and no licenses, return first error.
16823 return $api_errors[0];
16824 }
16825
16826 // Fallback to empty licenses list.
16827 return $result;
16828 }
16829
16830 /**
16831 * @author Vova Feldman (@svovaf)
16832 * @since 2.0.0
16833 *
16834 * @param number $license_id
16835 * @param string $license_key
16836 *
16837 * @return \FS_Plugin_License|object
16838 */
16839 private function fetch_license_by_key( $license_id, $license_key ) {
16840 $this->_logger->entrance();
16841
16842 $api = $this->get_current_or_network_user_api_scope();
16843
16844 $result = $api->get( "/licenses/{$license_id}.json?license_key=" . urlencode( $license_key ) );
16845
16846 return $this->is_api_result_entity( $result ) ?
16847 new FS_Plugin_License( $result ) :
16848 $result;
16849 }
16850
16851 /**
16852 * @author Vova Feldman (@svovaf)
16853 * @since 1.2.0
16854 * @uses FS_Api
16855 *
16856 * @param number|bool $plugin_id
16857 * @param bool $flush
16858 *
16859 * @return FS_Payment[]|object
16860 */
16861 function _fetch_payments( $plugin_id = false, $flush = false ) {
16862 $this->_logger->entrance();
16863
16864 $api = $this->get_api_user_scope();
16865
16866 if ( ! is_numeric( $plugin_id ) ) {
16867 $plugin_id = $this->_plugin->id;
16868 }
16869
16870 $result = $api->get( "/plugins/{$plugin_id}/payments.json?include_addons=true", $flush );
16871
16872 if ( ! isset( $result->error ) ) {
16873 for ( $i = 0, $len = count( $result->payments ); $i < $len; $i ++ ) {
16874 $result->payments[ $i ] = new FS_Payment( $result->payments[ $i ] );
16875 }
16876 $result = $result->payments;
16877 }
16878
16879 return $result;
16880 }
16881
16882 /**
16883 * @author Vova Feldman (@svovaf)
16884 * @since 1.2.1.5
16885 * @uses FS_Api
16886 *
16887 * @param bool $flush
16888 *
16889 * @return \FS_Billing|mixed
16890 */
16891 function _fetch_billing( $flush = false ) {
16892 require_once WP_FS__DIR_INCLUDES . '/entities/class-fs-billing.php';
16893
16894 $billing = $this->get_api_user_scope()->get( 'billing.json', $flush );
16895
16896 if ( $this->is_api_result_entity( $billing ) ) {
16897 $billing = new FS_Billing( $billing );
16898 }
16899
16900 return $billing;
16901 }
16902
16903 /**
16904 * @author Vova Feldman (@svovaf)
16905 * @since 1.0.5
16906 *
16907 * @param FS_Plugin_License[] $licenses
16908 * @param number $module_id
16909 */
16910 private function _update_licenses( $licenses, $module_id ) {
16911 $this->_logger->entrance();
16912
16913 if ( is_array( $licenses ) ) {
16914 for ( $i = 0, $len = count( $licenses ); $i < $len; $i ++ ) {
16915 $licenses[ $i ]->updated = time();
16916 }
16917 }
16918
16919 $this->_store_licenses( true, $module_id, $licenses );
16920 }
16921
16922 /**
16923 * @author Vova Feldman (@svovaf)
16924 * @since 1.0.4
16925 *
16926 * @param bool|number $plugin_id
16927 * @param bool $flush Since 1.1.7.3
16928 * @param int $expiration Since 1.2.2.7
16929 * @param bool|string $newer_than Since 2.2.1
16930 *
16931 * @return object|false New plugin tag info if exist.
16932 */
16933 private function _fetch_newer_version( $plugin_id = false, $flush = true, $expiration = WP_FS__TIME_24_HOURS_IN_SEC, $newer_than = false ) {
16934 $latest_tag = $this->_fetch_latest_version( $plugin_id, $flush, $expiration, $newer_than );
16935
16936 if ( ! is_object( $latest_tag ) ) {
16937 return false;
16938 }
16939
16940 // Check if version is actually newer.
16941 $has_new_version =
16942 // If it's an non-installed add-on then always return latest.
16943 ( $this->_is_addon_id( $plugin_id ) && ! $this->is_addon_activated( $plugin_id ) ) ||
16944 // Compare versions.
16945 version_compare( $this->get_plugin_version(), $latest_tag->version, '<' );
16946
16947 $this->_logger->departure( $has_new_version ? 'Found newer plugin version ' . $latest_tag->version : 'No new version' );
16948
16949 return $has_new_version ? $latest_tag : false;
16950 }
16951
16952 /**
16953 * @author Vova Feldman (@svovaf)
16954 * @since 1.0.5
16955 *
16956 * @param bool|number $plugin_id
16957 * @param bool $flush Since 1.1.7.3
16958 * @param int $expiration Since 1.2.2.7
16959 * @param bool|string $newer_than Since 2.2.1
16960 *
16961 * @return bool|FS_Plugin_Tag
16962 */
16963 function get_update( $plugin_id = false, $flush = true, $expiration = WP_FS__TIME_24_HOURS_IN_SEC, $newer_than = false ) {
16964 $this->_logger->entrance();
16965
16966 if ( ! is_numeric( $plugin_id ) ) {
16967 $plugin_id = $this->_plugin->id;
16968 }
16969
16970 $this->check_updates( true, $plugin_id, $flush, $expiration, $newer_than );
16971 $updates = $this->get_all_updates();
16972
16973 return isset( $updates[ $plugin_id ] ) && is_object( $updates[ $plugin_id ] ) ? $updates[ $plugin_id ] : false;
16974 }
16975
16976 /**
16977 * Check if site assigned with active license.
16978 *
16979 * @author Vova Feldman (@svovaf)
16980 * @since 1.0.6
16981 *
16982 * @deprecated Please use has_active_valid_license() instead because license can be cancelled.
16983 */
16984 function has_active_license() {
16985 return (
16986 is_object( $this->_license ) &&
16987 is_numeric( $this->_license->id ) &&
16988 ! $this->_license->is_expired()
16989 );
16990 }
16991
16992 /**
16993 * Check if site assigned with active & valid (not expired) license.
16994 *
16995 * @author Vova Feldman (@svovaf)
16996 * @since 1.2.1
16997 */
16998 function has_active_valid_license() {
16999 return self::is_active_valid_license( $this->_license );
17000 }
17001
17002 /**
17003 * Check if a given license is active & valid (not expired).
17004 *
17005 * @author Vova Feldman (@svovaf)
17006 * @since 2.1.3
17007 *
17008 * @param FS_Plugin_License $license
17009 *
17010 * @return bool
17011 */
17012 private static function is_active_valid_license( $license ) {
17013 return (
17014 is_object( $license ) &&
17015 FS_Plugin_License::is_valid_id( $license->id ) &&
17016 $license->is_active() &&
17017 $license->is_valid()
17018 );
17019 }
17020
17021 /**
17022 * Checks if there's any site that is associated with an active & valid license.
17023 * This logic is used to determine if the admin can download the premium code base from a network level admin.
17024 *
17025 * @author Vova Feldman (@svovaf)
17026 * @since 2.1.3
17027 *
17028 * @return bool
17029 */
17030 function has_any_active_valid_license() {
17031 if ( ! fs_is_network_admin() ) {
17032 return $this->has_active_valid_license();
17033 }
17034
17035 $installs = $this->get_blog_install_map();
17036 $all_plugin_licenses = self::get_all_licenses( $this->_module_id );
17037
17038 foreach ( $installs as $blog_id => $install ) {
17039 if ( ! FS_Plugin_License::is_valid_id( $install->license_id ) ) {
17040 continue;
17041 }
17042
17043 foreach ( $all_plugin_licenses as $license ) {
17044 if ( $license->id == $install->license_id ) {
17045 if ( self::is_active_valid_license( $license ) ) {
17046 return true;
17047 }
17048 }
17049 }
17050 }
17051
17052 return false;
17053 }
17054
17055 /**
17056 * Check if site assigned with license with enabled features.
17057 *
17058 * @author Vova Feldman (@svovaf)
17059 * @since 1.0.6
17060 *
17061 * @return bool
17062 */
17063 function has_features_enabled_license() {
17064 return (
17065 is_object( $this->_license ) &&
17066 is_numeric( $this->_license->id ) &&
17067 $this->_license->is_features_enabled()
17068 );
17069 }
17070
17071 /**
17072 * Check if user is a trial or have feature enabled license.
17073 *
17074 * @author Vova Feldman (@svovaf)
17075 * @since 1.1.7
17076 *
17077 * @return bool
17078 */
17079 function can_use_premium_code() {
17080 return $this->is_trial() || $this->has_features_enabled_license();
17081 }
17082
17083 /**
17084 * Checks if the current user can activate plugins or switch themes. Note that this method should only be used
17085 * after the `init` action is triggered because it is using `current_user_can()` which is only functional after
17086 * the context user is authenticated.
17087 *
17088 * @author Leo Fajardo (@leorw)
17089 * @since 1.2.2
17090 *
17091 * @return bool
17092 */
17093 function is_user_admin() {
17094 /**
17095 * Require a super-admin when network activated, running from the network level OR if
17096 * running from the site level but not delegated the opt-in.
17097 *
17098 * @author Vova Feldman (@svovaf)
17099 * @since 2.0.0
17100 */
17101 if ( $this->_is_network_active &&
17102 ( fs_is_network_admin() || ! $this->is_delegated_connection() )
17103 ) {
17104 return is_super_admin();
17105 }
17106
17107 return ( $this->is_plugin() && current_user_can( is_multisite() ? 'manage_options' : 'activate_plugins' ) )
17108 || ( $this->is_theme() && current_user_can( 'switch_themes' ) );
17109 }
17110
17111 /**
17112 * Sync site's plan.
17113 *
17114 * @author Vova Feldman (@svovaf)
17115 * @since 1.0.3
17116 *
17117 * @uses FS_Api
17118 *
17119 * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by
17120 * the admin.
17121 * @param bool $is_context_single_site @since 2.0.0. This is used when syncing a license for a single install from the
17122 * network-level "Account" page.
17123 * @param int|null $current_blog_id @since 2.2.3. This is passed from the `execute_cron` method and used by the
17124 * `_sync_plugin_license` method in order to switch to the previous blog when sending
17125 * updates for a single site in case `execute_cron` has switched to a different blog.
17126 */
17127 private function _sync_license( $background = false, $is_context_single_site = false, $current_blog_id = null ) {
17128 $this->_logger->entrance();
17129
17130 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
17131
17132 $is_addon_sync = ( ! $this->_plugin->is_addon() && $plugin_id != $this->get_id() );
17133
17134 if ( $is_addon_sync ) {
17135 $this->_sync_addon_license( $plugin_id, $background );
17136 } else {
17137 $this->_sync_plugin_license( $background, true, $is_context_single_site, $current_blog_id );
17138 }
17139
17140 $this->do_action( 'after_account_plan_sync', $this->get_plan_name() );
17141 }
17142
17143 /**
17144 * Sync plugin's add-on license.
17145 *
17146 * @author Vova Feldman (@svovaf)
17147 * @since 1.0.6
17148 * @uses FS_Api
17149 *
17150 * @param number $addon_id
17151 * @param bool $background
17152 */
17153 private function _sync_addon_license( $addon_id, $background ) {
17154 $this->_logger->entrance();
17155
17156 if ( $this->is_addon_activated( $addon_id ) ) {
17157 // If already installed, use add-on sync.
17158 $fs_addon = self::get_instance_by_id( $addon_id );
17159 $fs_addon->_sync_license( $background );
17160
17161 return;
17162 }
17163
17164 // Validate add-on exists.
17165 $addon = $this->get_addon( $addon_id );
17166
17167 if ( ! is_object( $addon ) ) {
17168 return;
17169 }
17170
17171 // Add add-on into account add-ons.
17172 $account_addons = $this->get_account_addons();
17173 if ( ! is_array( $account_addons ) ) {
17174 $account_addons = array();
17175 }
17176 $account_addons[] = $addon->id;
17177 $account_addons = array_unique( $account_addons );
17178 $this->_store_account_addons( $account_addons );
17179
17180 // Load add-on licenses.
17181 $licenses = $this->_fetch_licenses( $addon->id );
17182
17183 // Sync add-on licenses.
17184 if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) {
17185 $this->_update_licenses( $licenses, $addon->id );
17186
17187 if ( ! $this->is_addon_installed( $addon->id ) && FS_License_Manager::has_premium_license( $licenses ) ) {
17188 $plans_result = $this->get_api_site_or_plugin_scope()->get( $this->add_show_pending( "/addons/{$addon_id}/plans.json" ) );
17189
17190 if ( ! isset( $plans_result->error ) ) {
17191 $plans = array();
17192 foreach ( $plans_result->plans as $plan ) {
17193 $plans[] = new FS_Plugin_Plan( $plan );
17194 }
17195
17196 $this->_admin_notices->add_sticky(
17197 sprintf(
17198 ( FS_Plan_Manager::instance()->has_free_plan( $plans ) ?
17199 $this->get_text_inline( 'Your %s Add-on plan was successfully upgraded.', 'addon-successfully-upgraded-message' ) :
17200 /* translators: %s:product name, e.g. Facebook add-on was successfully... */
17201 $this->get_text_inline( '%s Add-on was successfully purchased.', 'addon-successfully-purchased-message' ) ),
17202 $addon->title
17203 ) . ' ' . $this->get_latest_download_link(
17204 $this->get_text_inline( 'Download the latest version', 'download-latest-version' ),
17205 $addon_id
17206 ),
17207 'addon_plan_upgraded_' . $addon->slug,
17208 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
17209 );
17210 }
17211 }
17212 }
17213 }
17214
17215 /**
17216 * Sync site's plugin plan.
17217 *
17218 * @author Vova Feldman (@svovaf)
17219 * @since 1.0.6
17220 * @uses FS_Api
17221 *
17222 * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by the admin.
17223 * @param bool $send_installs_update Since 2.0.0
17224 * @param bool $is_context_single_site Since 2.0.0. This is used when sending an update for a single install and
17225 * syncing its license from the network-level "Account" page (e.g.: after
17226 * activating a license only for the single install).
17227 * @param int|null $current_blog_id Since 2.2.3. This is passed from the `execute_cron` method so that it
17228 * can be used here to switch to the previous blog in case `execute_cron`
17229 * has switched to a different blog.
17230 */
17231 private function _sync_plugin_license(
17232 $background = false,
17233 $send_installs_update = true,
17234 $is_context_single_site = false,
17235 $current_blog_id = null
17236 ) {
17237 $this->_logger->entrance();
17238
17239 $plan_change = 'none';
17240
17241 $is_site_level_sync = ( $is_context_single_site || fs_is_blog_admin() || ! $this->_is_network_active );
17242
17243 if ( ! $send_installs_update ) {
17244 $site = $this->_site;
17245 } else {
17246 /**
17247 * Sync site info.
17248 *
17249 * @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.
17250 */
17251 if ( $is_site_level_sync ) {
17252 /**
17253 * Switch to the previous blog since `execute_cron` may have switched to a different blog.
17254 *
17255 * @author Leo Fajardo (@leorw)
17256 * @since 2.2.3
17257 */
17258 if ( is_numeric( $current_blog_id ) ) {
17259 $this->switch_to_blog( $current_blog_id );
17260 }
17261
17262 $result = $this->send_install_update( array(), true );
17263 $is_valid = $this->is_api_result_entity( $result );
17264 } else {
17265 $result = $this->send_installs_update( array(), true );
17266 $is_valid = $this->is_api_result_object( $result, 'installs' );
17267 }
17268
17269 if ( ! $is_valid ) {
17270 if ( $is_context_single_site ) {
17271 // Switch back to the main blog so that the following logic will have the right entities.
17272 $this->switch_to_blog( $this->_storage->network_install_blog_id );
17273 }
17274
17275 // Show API messages only if not background sync or if paying customer.
17276 if ( ! $background || $this->is_paying() ) {
17277 // Try to ping API to see if not blocked.
17278 if ( ! FS_Api::test() ) {
17279 /**
17280 * Failed to ping API - blocked!
17281 *
17282 * @author Vova Feldman (@svovaf)
17283 * @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.
17284 */
17285 $api = $this->get_api_site_scope();
17286
17287 if ( ! self::$_global_admin_notices->has_sticky( 'api_blocked' ) ) {
17288 self::$_global_admin_notices->add(
17289 sprintf(
17290 $this->get_text_x_inline( 'Your server is blocking the access to Freemius\' API, which is crucial for %1s synchronization. Please contact your host to whitelist %2s', '%1s - plugin title, %2s - API domain', 'server-blocking-access' ),
17291 $this->get_plugin_name(),
17292 '<a href="' . $api->get_url() . '" target="_blank">' . $api->get_url() . '</a>'
17293 ) . '<br> ' . $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . var_export( $result->error, true ),
17294 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
17295 'error',
17296 $background,
17297 'api_blocked'
17298 );
17299 }
17300 } else {
17301 // Authentication params are broken.
17302 $this->_admin_notices->add(
17303 $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' ),
17304 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
17305 'error'
17306 );
17307 }
17308 }
17309
17310 // No reason to continue with license sync while there are API issues.
17311 return;
17312 }
17313
17314 if ( $is_site_level_sync ) {
17315 $site = new FS_Site( $result );
17316 } else {
17317 // Map site addresses to their blog IDs.
17318 $address_to_blog_map = $this->get_address_to_blog_map();
17319
17320 // Find the current context install.
17321 $site = null;
17322 foreach ( $result->installs as $install ) {
17323 if ( $install->id == $this->_site->id ) {
17324 $site = new FS_Site( $install );
17325 } else {
17326 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
17327 $blog_id = $address_to_blog_map[ $address ];
17328
17329 $this->_store_site( true, $blog_id, new FS_Site( $install ) );
17330 }
17331 }
17332 }
17333
17334 // Sync plans.
17335 $this->_sync_plans();
17336 }
17337
17338 // Remove sticky API connectivity message.
17339 self::$_global_admin_notices->remove_sticky( 'api_blocked' );
17340
17341 if ( ! $this->has_paid_plan() ) {
17342 $this->_site = $site;
17343 $this->_store_site(
17344 true,
17345 $is_site_level_sync ?
17346 null :
17347 $this->get_network_install_blog_id()
17348 );
17349 } else {
17350 $context_blog_id = 0;
17351
17352 if ( $is_context_single_site ) {
17353 $context_blog_id = get_current_blog_id();
17354
17355 // Switch back to the main blog in order to properly sync the license.
17356 $this->switch_to_blog( $this->_storage->network_install_blog_id );
17357 }
17358
17359 /**
17360 * Sync licenses. Pass the site's license ID so that the foreign licenses will be fetched if the license
17361 * associated with that ID is not included in the user's licenses collection.
17362 */
17363 $this->_sync_licenses(
17364 $site->license_id,
17365 ( $is_context_single_site ?
17366 $context_blog_id :
17367 null
17368 )
17369 );
17370
17371 if ( $is_context_single_site ) {
17372 $this->switch_to_blog( $context_blog_id );
17373 }
17374
17375 // Check if plan / license changed.
17376 if ( $site->plan_id != $this->_site->plan_id ||
17377 // Check if trial started.
17378 $site->trial_plan_id != $this->_site->trial_plan_id ||
17379 $site->trial_ends != $this->_site->trial_ends ||
17380 // Check if license changed.
17381 $site->license_id != $this->_site->license_id
17382 ) {
17383 if ( $site->is_trial() && ( ! $this->_site->is_trial() || $site->trial_ends != $this->_site->trial_ends ) ) {
17384 // New trial started.
17385 $this->_site = $site;
17386 $plan_change = 'trial_started';
17387
17388 // For trial with subscription use-case.
17389 $new_license = is_null( $site->license_id ) ? null : $this->_get_license_by_id( $site->license_id );
17390
17391 if ( is_object( $new_license ) && $new_license->is_valid() ) {
17392 $this->_site = $site;
17393 $this->_update_site_license( $new_license );
17394 $this->_store_licenses();
17395
17396 $this->_sync_site_subscription( $this->_license );
17397 }
17398 } else if ( $this->_site->is_trial() && ! $site->is_trial() && ! is_numeric( $site->license_id ) ) {
17399 // Was in trial, but now trial expired and no license ID.
17400 // New trial started.
17401 $this->_site = $site;
17402 $plan_change = 'trial_expired';
17403 } else {
17404 $is_free = $this->is_free_plan();
17405
17406 // Make sure license exist and not expired.
17407 $new_license = is_null( $site->license_id ) ?
17408 null :
17409 $this->_get_license_by_id( $site->license_id );
17410
17411 if ( $is_free && is_null( $new_license ) && $this->has_any_license() && $this->_license->is_cancelled ) {
17412 // License cancelled.
17413 $this->_site = $site;
17414 $this->_update_site_license( $new_license );
17415 $this->_store_licenses();
17416
17417 $plan_change = 'cancelled';
17418 } else if ( $is_free && ( ( ! is_object( $new_license ) || $new_license->is_expired() ) ) ) {
17419 // The license is expired, so ignore upgrade method.
17420 $this->_site = $site;
17421 } else {
17422 // License changed.
17423 $this->_site = $site;
17424
17425 /**
17426 * IMPORTANT:
17427 * 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.
17428 *
17429 * @author Vova Feldman (@svovaf)
17430 * @since 2.0.0
17431 */
17432 $this->_update_site_license( $new_license );
17433
17434 if ( ! $is_context_single_site &&
17435 fs_is_network_admin() &&
17436 $this->_is_network_active &&
17437 $new_license->quota > 1 &&
17438 get_blog_count() > 1
17439 ) {
17440 // See if license can activated on all sites.
17441 if ( ! $this->try_activate_license_on_network( $this->_user, $new_license ) ) {
17442 if ( ! fs_request_get_bool( 'auto_install' ) ) {
17443 // Open the license activation dialog box on the account page.
17444 add_action( 'admin_footer', array(
17445 &$this,
17446 '_open_license_activation_dialog_box'
17447 ) );
17448 }
17449 }
17450 }
17451
17452 $this->_store_licenses();
17453
17454 $plan_change = $is_free ?
17455 'upgraded' :
17456 ( is_object( $new_license ) ?
17457 'changed' :
17458 'downgraded' );
17459 }
17460 }
17461
17462 // Store updated site info.
17463 $this->_store_site(
17464 true,
17465 $is_site_level_sync ?
17466 null :
17467 $this->get_network_install_blog_id()
17468 );
17469 } else {
17470 if ( is_object( $this->_license ) && $this->_license->is_expired() ) {
17471 if ( ! $this->has_features_enabled_license() ) {
17472 $this->_deactivate_license();
17473 $plan_change = 'downgraded';
17474 } else {
17475 $plan_change = 'expired';
17476 }
17477 }
17478
17479 if ( is_numeric( $site->license_id ) && is_object( $this->_license ) ) {
17480 $this->_sync_site_subscription( $this->_license );
17481 }
17482 }
17483 }
17484
17485 $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...';
17486
17487 if ( $this->has_paid_plan() ) {
17488 switch ( $plan_change ) {
17489 case 'none':
17490 if ( ! $background && is_admin() ) {
17491 $plan = $this->is_trial() ?
17492 $this->get_trial_plan() :
17493 $this->get_plan();
17494
17495 if ( $plan->is_free() ) {
17496 $this->_admin_notices->add(
17497 sprintf(
17498 $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' ),
17499 '<i><b>' . $plan->title . ( $this->is_trial() ? ' ' . $this->get_text_x_inline( 'Trial', 'trial period', 'trial' ) : '' ) . '</b></i>'
17500 ) . ' ' . sprintf(
17501 '<a href="%s">%s</a>',
17502 $this->contact_url(
17503 'bug',
17504 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' ),
17505 strtoupper( $plan->name )
17506 )
17507 ),
17508 $this->get_text_inline( 'Please contact us here', 'contact-us-here' )
17509 ),
17510 $hmm_text
17511 );
17512 }
17513 }
17514 break;
17515 case 'upgraded':
17516 $this->_admin_notices->add_sticky(
17517 sprintf(
17518 $this->get_text_inline( 'Your plan was successfully upgraded.', 'plan-upgraded-message' ),
17519 '<i>' . $this->get_plugin_name() . '</i>'
17520 ) . $this->get_complete_upgrade_instructions(),
17521 'plan_upgraded',
17522 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
17523 );
17524
17525 $this->_admin_notices->remove_sticky( array(
17526 'trial_started',
17527 'trial_promotion',
17528 'trial_expired',
17529 'activation_complete',
17530 'license_expired',
17531 ) );
17532 break;
17533 case 'changed':
17534 $this->_admin_notices->add_sticky(
17535 sprintf(
17536 $this->get_text_inline( 'Your plan was successfully changed to %s.', 'plan-changed-to-x-message' ),
17537 $this->get_plan_title()
17538 ),
17539 'plan_changed'
17540 );
17541
17542 $this->_admin_notices->remove_sticky( array(
17543 'trial_started',
17544 'trial_promotion',
17545 'trial_expired',
17546 'activation_complete',
17547 ) );
17548 break;
17549 case 'downgraded':
17550 $this->_admin_notices->add_sticky(
17551 ($this->has_free_plan() ?
17552 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 ) :
17553 /* translators: %1$s: product title; %2$s, %3$s: wrapping HTML anchor element; %4$s: 'plugin', 'theme', or 'add-on'. */
17554 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) ) ),
17555 'license_expired',
17556 $hmm_text
17557 );
17558 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
17559 break;
17560 case 'cancelled':
17561 $this->_admin_notices->add(
17562 $this->get_text_inline( 'Your license has been cancelled. If you think it\'s a mistake, please contact support.', 'license-cancelled' ) . ' ' .
17563 sprintf(
17564 '<a href="%s">%s</a>',
17565 $this->contact_url( 'bug' ),
17566 $this->get_text_inline( 'Please contact us here', 'contact-us-here' )
17567 ),
17568 $hmm_text,
17569 'error'
17570 );
17571 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
17572 break;
17573 case 'expired':
17574 $this->_admin_notices->add_sticky(
17575 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 ),
17576 'license_expired',
17577 $hmm_text
17578 );
17579 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
17580 break;
17581 case 'trial_started':
17582 $this->_admin_notices->add_sticky(
17583 sprintf(
17584 $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ),
17585 '<i>' . $this->get_plugin_name() . '</i>'
17586 ) . $this->get_complete_upgrade_instructions( $this->get_trial_plan()->title ),
17587 'trial_started',
17588 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
17589 );
17590
17591 $this->_admin_notices->remove_sticky( array(
17592 'trial_promotion',
17593 ) );
17594 break;
17595 case 'trial_expired':
17596 $this->_admin_notices->add_sticky(
17597 ($this->has_free_plan() ?
17598 $this->get_text_inline( 'Your free trial has expired. You can still continue using all our free features.', 'trial-expired-message' ) :
17599 /* translators: %1$s: product title; %2$s, %3$s: wrapping HTML anchor element; %4$s: 'plugin', 'theme', or 'add-on'. */
17600 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))),
17601 'trial_expired',
17602 $hmm_text
17603 );
17604 $this->_admin_notices->remove_sticky( array(
17605 'trial_started',
17606 'trial_promotion',
17607 'plan_upgraded',
17608 ) );
17609 break;
17610 }
17611 }
17612
17613 if ( 'none' !== $plan_change ) {
17614 $this->do_action( 'after_license_change', $plan_change, $this->get_plan() );
17615 }
17616 }
17617
17618 /**
17619 * Include the required JS at the footer of the admin to trigger the license activation dialog box.
17620 *
17621 * @author Vova Feldman (@svovaf)
17622 * @since 2.0.0
17623 */
17624 public function _open_license_activation_dialog_box() {
17625 $vars = array( 'license_id' => $this->_site->license_id );
17626 fs_require_once_template( 'js/open-license-activation.php', $vars );
17627 }
17628
17629 /**
17630 * @author Vova Feldman (@svovaf)
17631 * @since 1.0.5
17632 *
17633 * @param bool $background
17634 */
17635 protected function _activate_license( $background = false ) {
17636 $this->_logger->entrance();
17637
17638 $license_id = fs_request_get( 'license_id' );
17639
17640 if ( is_object( $this->_site ) &&
17641 FS_Plugin_License::is_valid_id( $license_id ) &&
17642 $license_id == $this->_site->license_id
17643 ) {
17644 // License is already activated.
17645 return;
17646 }
17647
17648 $premium_license = FS_Plugin_License::is_valid_id( $license_id ) ?
17649 $this->_get_license_by_id( $license_id ) :
17650 $this->_get_available_premium_license();
17651
17652 if ( ! is_object( $premium_license ) ) {
17653 return;
17654 }
17655
17656 if ( ! is_object( $this->_site ) ) {
17657 // Not yet opted-in.
17658 $user = $this->get_current_or_network_user();
17659 if ( ! is_object( $user ) ) {
17660 $user = self::_get_user_by_id( $premium_license->user_id );
17661 }
17662
17663 if ( is_object( $user ) ) {
17664 $this->install_with_user( $user, $premium_license->secret_key, false, false, false );
17665 } else {
17666 $this->opt_in(
17667 false,
17668 false,
17669 false,
17670 $premium_license->secret_key
17671 );
17672
17673 return;
17674 }
17675 }
17676
17677
17678 /**
17679 * If the premium license is already associated with the install, just
17680 * update the license reference (activation is not required).
17681 *
17682 * @since 1.1.9
17683 */
17684 if ( $premium_license->id == $this->_site->license_id ) {
17685 // License is already activated.
17686 $this->_update_site_license( $premium_license );
17687 $this->_store_account();
17688
17689 return;
17690 }
17691
17692 if ( $this->_site->user_id != $premium_license->user_id ) {
17693 $api_request_params = array( 'license_key' => $premium_license->secret_key );
17694 } else {
17695 $api_request_params = array();
17696 }
17697
17698 $api = $this->get_api_site_scope();
17699 $license = $api->call( "/licenses/{$premium_license->id}.json", 'put', $api_request_params );
17700
17701 if ( ! $this->is_api_result_entity( $license ) ) {
17702 if ( ! $background ) {
17703 $this->_admin_notices->add( sprintf(
17704 '%s %s',
17705 $this->get_text_inline( 'It looks like the license could not be activated.', 'license-activation-failed-message' ),
17706 ( is_object( $license ) && isset( $license->error ) ?
17707 $license->error->message :
17708 sprintf( '%s<br><code>%s</code>',
17709 $this->get_text_inline( 'Error received from the server:', 'server-error-message' ),
17710 var_export( $license, true )
17711 )
17712 )
17713 ),
17714 $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...',
17715 'error'
17716 );
17717 }
17718
17719 return;
17720 }
17721
17722 $premium_license = new FS_Plugin_License( $license );
17723
17724 // Updated site plan.
17725 $site = $this->get_api_site_scope()->get( '/', true );
17726 if ( $this->is_api_result_entity( $site ) ) {
17727 $this->_site = new FS_Site( $site );
17728 }
17729 $this->_update_site_license( $premium_license );
17730
17731 $this->_store_account();
17732
17733 if ( ! $background ) {
17734 $this->_admin_notices->add_sticky(
17735 $this->get_text_inline( 'Your license was successfully activated.', 'license-activated-message' ) .
17736 $this->get_complete_upgrade_instructions(),
17737 'license_activated',
17738 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
17739 );
17740 }
17741
17742 $this->_admin_notices->remove_sticky( array(
17743 'trial_promotion',
17744 'license_expired',
17745 ) );
17746 }
17747
17748 /**
17749 * @author Vova Feldman (@svovaf)
17750 * @since 1.0.5
17751 *
17752 * @param bool $show_notice
17753 */
17754 protected function _deactivate_license( $show_notice = true ) {
17755 $this->_logger->entrance();
17756
17757 $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...';
17758
17759 if ( ! FS_Plugin_License::is_valid_id( $this->_site->license_id ) ) {
17760 $this->_admin_notices->add(
17761 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() ),
17762 $hmm_text
17763 );
17764
17765 return;
17766 }
17767
17768 $api = $this->get_api_site_scope();
17769 $license = $api->call( "/licenses/{$this->_site->license_id}.json", 'delete' );
17770
17771 $this->handle_license_deactivation_result( $license, $hmm_text, $show_notice );
17772 }
17773
17774 /**
17775 * @author Leo Fajardo (@leorw)
17776 * @since 2.2.1
17777 *
17778 * @param FS_Plugin_License $license
17779 * @param bool|string $hmm_text
17780 * @param bool $show_notice
17781 */
17782 private function handle_license_deactivation_result( $license, $hmm_text = false, $show_notice = true ) {
17783 if ( isset( $license->error ) ) {
17784 $this->_admin_notices->add(
17785 $this->get_text_inline( 'It looks like the license deactivation failed.', 'license-deactivation-failed-message' ) . '<br> ' .
17786 $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . ' ' . var_export( $license->error, true ),
17787 $hmm_text,
17788 'error'
17789 );
17790
17791 return;
17792 }
17793
17794 // Update license cache.
17795 if ( is_array( $this->_licenses ) ) {
17796 for ( $i = 0, $len = count( $this->_licenses ); $i < $len; $i ++ ) {
17797 if ( $license->id == $this->_licenses[ $i ]->id ) {
17798 $this->_licenses[ $i ] = new FS_Plugin_License( $license );
17799 }
17800 }
17801 }
17802
17803 // Update site plan to default.
17804 $this->_sync_plans();
17805 $this->_site->plan_id = $this->_plans[0]->id;
17806 // Unlink license from site.
17807 $this->_update_site_license( null );
17808
17809 $this->_store_account();
17810
17811 if ( $show_notice ) {
17812 $this->_admin_notices->add(
17813 sprintf( $this->get_text_inline( 'Your license was successfully deactivated, you are back to the %s plan.', 'license-deactivation-message' ), $this->get_plan_title() ),
17814 $this->get_text_inline( 'O.K', 'ok' )
17815 );
17816 }
17817
17818 $this->_admin_notices->remove_sticky( array(
17819 'plan_upgraded',
17820 'license_activated',
17821 ) );
17822 }
17823
17824 /**
17825 * Site plan downgrade.
17826 *
17827 * @author Vova Feldman (@svovaf)
17828 * @since 1.0.4
17829 *
17830 * @return object
17831 *
17832 * @uses FS_Api
17833 */
17834 private function _downgrade_site() {
17835 $this->_logger->entrance();
17836
17837 $deactivate_license = fs_request_get_bool( 'deactivate_license' );
17838
17839 $api = $this->get_api_site_scope();
17840 $site = $api->call( 'downgrade.json', 'put', array( 'deactivate_license' => $deactivate_license ) );
17841
17842 $plan_downgraded = false;
17843 $plan = false;
17844 if ( $this->is_api_result_entity( $site ) ) {
17845 $prev_plan_id = $this->_site->plan_id;
17846
17847 // Update new site plan id.
17848 $this->_site->plan_id = $site->plan_id;
17849
17850 $plan = $this->get_plan();
17851 $subscription = $this->_sync_site_subscription( $this->_license );
17852
17853 // Plan downgraded if plan was changed or subscription was cancelled.
17854 $plan_downgraded = ( $plan instanceof FS_Plugin_Plan && $prev_plan_id != $plan->id ) ||
17855 ( is_object( $subscription ) && ! isset( $subscription->error ) && ! $subscription->is_active() );
17856 } else {
17857 // handle different error cases.
17858 $this->handle_license_deactivation_result(
17859 $site,
17860 $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...'
17861 );
17862 }
17863
17864 if ( ! $plan_downgraded ) {
17865 return (object) array(
17866 'error' => (object) array(
17867 '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' )
17868 )
17869 );
17870 }
17871
17872 // Remove previous sticky message about upgrade (if exist).
17873 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
17874
17875 $this->_admin_notices->add(
17876 sprintf( $this->get_text_inline( 'Your subscription was successfully cancelled. Your %s plan license will expire in %s.', 'plan-x-downgraded-message' ),
17877 $plan->title,
17878 human_time_diff( time(), strtotime( $this->_license->expiration ) )
17879 )
17880 );
17881
17882 // Store site updates.
17883 $this->_store_site();
17884
17885 if ( $deactivate_license &&
17886 ! FS_Plugin_License::is_valid_id( $site->license_id )
17887 ) {
17888 if ( $this->_site->is_localhost() ) {
17889 $this->_license->activated_local = max( 0, $this->_license->activated_local - 1 );
17890 } else {
17891 $this->_license->activated = max( 0, $this->_license->activated - 1 );
17892 }
17893
17894 // Handle successful license deactivation result.
17895 $this->handle_license_deactivation_result( $this->_license );
17896 }
17897
17898 return $site;
17899 }
17900
17901 /**
17902 * @author Vova Feldman (@svovaf)
17903 * @since 1.1.8.1
17904 *
17905 * @param bool|string $plan_name
17906 *
17907 * @return bool If trial was successfully started.
17908 */
17909 function start_trial( $plan_name = false ) {
17910 $this->_logger->entrance();
17911
17912 // Alias.
17913 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
17914
17915 if ( $this->is_trial() ) {
17916 // Already in trial mode.
17917 $this->_admin_notices->add(
17918 sprintf( $this->get_text_inline( 'You are already running the %s in a trial mode.', 'in-trial-mode' ), $this->_module_type ),
17919 $oops_text,
17920 'error'
17921 );
17922
17923 return false;
17924 }
17925
17926 if ( $this->_site->is_trial_utilized() ) {
17927 // Trial was already utilized.
17928 $this->_admin_notices->add(
17929 $this->get_text_inline( 'You already utilized a trial before.', 'trial-utilized' ),
17930 $oops_text,
17931 'error'
17932 );
17933
17934 return false;
17935 }
17936
17937 if ( false !== $plan_name ) {
17938 $plan = $this->get_plan_by_name( $plan_name );
17939
17940 if ( false === $plan ) {
17941 // Plan doesn't exist.
17942 $this->_admin_notices->add(
17943 sprintf( $this->get_text_inline( 'Plan %s do not exist, therefore, can\'t start a trial.', 'trial-plan-x-not-exist' ), $plan_name ),
17944 $oops_text,
17945 'error'
17946 );
17947
17948 return false;
17949 }
17950
17951 if ( ! $plan->has_trial() ) {
17952 // Plan doesn't exist.
17953 $this->_admin_notices->add(
17954 sprintf( $this->get_text_inline( 'Plan %s does not support a trial period.', 'plan-x-no-trial' ), $plan_name ),
17955 $oops_text,
17956 'error'
17957 );
17958
17959 return false;
17960 }
17961 } else {
17962 if ( ! $this->has_trial_plan() ) {
17963 // None of the plans have a trial.
17964 $this->_admin_notices->add(
17965 sprintf( $this->get_text_inline( 'None of the %s\'s plans supports a trial period.', 'no-trials' ), $this->_module_type ),
17966 $oops_text,
17967 'error'
17968 );
17969
17970 return false;
17971 }
17972
17973 $plans_with_trial = FS_Plan_Manager::instance()->get_trial_plans( $this->_plans );
17974
17975 $plan = $plans_with_trial[0];
17976 }
17977
17978 $api = $this->get_api_site_scope();
17979 $plan = $api->call( "plans/{$plan->id}/trials.json", 'post' );
17980
17981 if ( ! $this->is_api_result_entity( $plan ) ) {
17982 // Some API error while trying to start the trial.
17983 $this->_admin_notices->add(
17984 sprintf( $this->get_text_inline( 'Unexpected API error. Please contact the %s\'s author with the following error.', 'unexpected-api-error' ), $this->_module_type )
17985 . ' ' . var_export( $plan, true ),
17986 $oops_text,
17987 'error'
17988 );
17989
17990 return false;
17991 }
17992
17993 // Sync license.
17994 $this->_sync_license();
17995
17996 return $this->is_trial();
17997 }
17998
17999 /**
18000 * Cancel site trial.
18001 *
18002 * @author Vova Feldman (@svovaf)
18003 * @since 1.0.9
18004 *
18005 * @return object
18006 *
18007 * @uses FS_Api
18008 */
18009 private function _cancel_trial() {
18010 $this->_logger->entrance();
18011
18012 if ( ! $this->is_trial() ) {
18013 return (object) array(
18014 'error' => (object) array(
18015 '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' )
18016 )
18017 );
18018 }
18019
18020 $trial_plan = $this->get_trial_plan();
18021
18022 $api = $this->get_api_site_scope();
18023 $site = $api->call( 'trials.json', 'delete' );
18024
18025 $trial_cancelled = false;
18026
18027 if ( $this->is_api_result_entity( $site ) ) {
18028 $prev_trial_ends = $this->_site->trial_ends;
18029
18030 if ( $this->is_paid_trial() ) {
18031 $this->_license->expiration = $site->trial_ends;
18032 $this->_license->is_cancelled = true;
18033 $this->_update_site_license( $this->_license );
18034 $this->_store_licenses();
18035
18036 // Clear subscription reference.
18037 $this->_sync_site_subscription( null );
18038 }
18039
18040 // Update site info.
18041 $this->_site = new FS_Site( $site );
18042
18043 $trial_cancelled = ( $prev_trial_ends != $site->trial_ends );
18044 } else {
18045 // @todo handle different error cases.
18046 }
18047
18048 if ( ! $trial_cancelled ) {
18049 return (object) array(
18050 'error' => (object) array(
18051 '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' )
18052 )
18053 );
18054 }
18055
18056 // Remove previous sticky messages about upgrade or trial (if exist).
18057 $this->_admin_notices->remove_sticky( array(
18058 'trial_started',
18059 'trial_promotion',
18060 'plan_upgraded',
18061 ) );
18062
18063 // Store site updates.
18064 $this->_store_site();
18065
18066 if ( ! $this->is_addon() ||
18067 ! $this->deactivate_premium_only_addon_without_license( true )
18068 ) {
18069 $this->_admin_notices->add(
18070 sprintf( $this->get_text_inline( 'Your %s free trial was successfully cancelled.', 'trial-cancel-message' ), $trial_plan->title )
18071 );
18072 }
18073
18074 return $site;
18075 }
18076
18077 /**
18078 * @author Vova Feldman (@svovaf)
18079 * @since 1.0.6
18080 *
18081 * @param bool|number $plugin_id
18082 *
18083 * @return bool
18084 */
18085 private function _is_addon_id( $plugin_id ) {
18086 return is_numeric( $plugin_id ) && ( $this->get_id() != $plugin_id );
18087 }
18088
18089 /**
18090 * Check if user eligible to download premium version updates.
18091 *
18092 * @author Vova Feldman (@svovaf)
18093 * @since 1.0.6
18094 *
18095 * @return bool
18096 */
18097 private function _can_download_premium() {
18098 return $this->has_any_active_valid_license() ||
18099 ( $this->is_trial() && ! $this->get_trial_plan()->is_free() );
18100 }
18101
18102 /**
18103 *
18104 * @author Vova Feldman (@svovaf)
18105 * @since 1.0.6
18106 *
18107 * @param bool|number $addon_id
18108 * @param string $type "json" or "zip"
18109 *
18110 * @return string
18111 */
18112 private function _get_latest_version_endpoint( $addon_id = false, $type = 'json' ) {
18113
18114 $is_addon = $this->_is_addon_id( $addon_id );
18115
18116 $is_premium = null;
18117 if ( ! $is_addon ) {
18118 $is_premium = ( $this->is_premium() || $this->_can_download_premium() );
18119 } else if ( $this->is_addon_activated( $addon_id ) ) {
18120 $fs_addon = self::get_instance_by_id( $addon_id );
18121 $is_premium = ( $fs_addon->is_premium() || $fs_addon->_can_download_premium() );
18122 }
18123
18124 // If add-on, then append add-on ID.
18125 $endpoint = ( $is_addon ? "/addons/$addon_id" : '' ) .
18126 '/updates/latest.' . $type;
18127
18128 // If add-on and not yet activated, try to fetch based on server licensing.
18129 if ( is_bool( $is_premium ) ) {
18130 $endpoint = add_query_arg( 'is_premium', json_encode( $is_premium ), $endpoint );
18131 }
18132
18133 if ( $this->has_secret_key() ) {
18134 $endpoint = add_query_arg( 'type', 'all', $endpoint );
18135 }
18136
18137 return $endpoint;
18138 }
18139
18140 /**
18141 * @author Vova Feldman (@svovaf)
18142 * @since 1.0.4
18143 *
18144 * @param bool|number $addon_id
18145 * @param bool $flush Since 1.1.7.3
18146 * @param int $expiration Since 1.2.2.7
18147 * @param bool|string $newer_than Since 2.2.1
18148 * @param bool|string $fetch_readme Since 2.2.1
18149 *
18150 * @return object|false Plugin latest tag info.
18151 */
18152 function _fetch_latest_version(
18153 $addon_id = false,
18154 $flush = true,
18155 $expiration = WP_FS__TIME_24_HOURS_IN_SEC,
18156 $newer_than = false,
18157 $fetch_readme = true
18158 ) {
18159 $this->_logger->entrance();
18160
18161 $switch_to_blog_id = null;
18162
18163 /**
18164 * @since 1.1.7.3 Check for plugin updates from Freemius only if opted-in.
18165 * @since 1.1.7.4 Also check updates for add-ons.
18166 */
18167 if ( ! $this->is_registered() &&
18168 ! $this->_is_addon_id( $addon_id )
18169 ) {
18170 if ( ! is_multisite() ) {
18171 return false;
18172 }
18173
18174 $installs_map = $this->get_blog_install_map();
18175
18176 foreach ( $installs_map as $blog_id => $install ) {
18177 /**
18178 * @var FS_Site $install
18179 */
18180 if ( $install->is_trial() ) {
18181 $switch_to_blog_id = $blog_id;
18182 break;
18183 }
18184
18185 if ( FS_Plugin_License::is_valid_id( $install->license_id ) ) {
18186 $license = $this->get_license_by_id( $install->license_id );
18187
18188 if ( is_object( $license ) && $license->is_features_enabled() ) {
18189 $switch_to_blog_id = $blog_id;
18190 break;
18191 }
18192 }
18193 }
18194
18195 if ( is_null( $switch_to_blog_id ) ) {
18196 return false;
18197 }
18198 }
18199
18200 $current_blog_id = is_numeric( $switch_to_blog_id ) ?
18201 get_current_blog_id() :
18202 0;
18203
18204 if ( is_numeric( $switch_to_blog_id ) ) {
18205 $this->switch_to_blog( $switch_to_blog_id );
18206 }
18207
18208 $latest_version_endpoint = $this->_get_latest_version_endpoint( $addon_id, 'json' );
18209
18210 if ( ! empty( $newer_than ) ) {
18211 $latest_version_endpoint = add_query_arg( 'newer_than', $newer_than, $latest_version_endpoint );
18212 }
18213
18214 if ( true === $fetch_readme ) {
18215 $latest_version_endpoint = add_query_arg( 'readme', 'true', $latest_version_endpoint );
18216 }
18217
18218 $tag = $this->get_api_site_or_plugin_scope()->get(
18219 $latest_version_endpoint,
18220 $flush,
18221 $expiration
18222 );
18223
18224 if ( is_numeric( $switch_to_blog_id ) ) {
18225 $this->switch_to_blog( $current_blog_id );
18226 }
18227
18228 $latest_version = ( is_object( $tag ) && isset( $tag->version ) ) ? $tag->version : 'couldn\'t get';
18229
18230 $this->_logger->departure( 'Latest version ' . $latest_version );
18231
18232 return ( is_object( $tag ) && isset( $tag->version ) ) ? $tag : false;
18233 }
18234
18235 #----------------------------------------------------------------------------------
18236 #region Download Plugin
18237 #----------------------------------------------------------------------------------
18238
18239 /**
18240 * Download latest plugin version, based on plan.
18241 *
18242 * Not like _download_latest(), this will redirect the page
18243 * to secure download url to prevent dual download (from FS to WP server,
18244 * and then from WP server to the client / browser).
18245 *
18246 * @author Vova Feldman (@svovaf)
18247 * @since 1.0.9
18248 *
18249 * @param bool|number $plugin_id
18250 *
18251 * @uses FS_Api
18252 * @uses wp_redirect()
18253 */
18254 private function download_latest_directly( $plugin_id = false ) {
18255 $this->_logger->entrance();
18256
18257 wp_redirect( $this->get_latest_download_api_url( $plugin_id ) );
18258 }
18259
18260 /**
18261 * Get latest plugin FS API download URL.
18262 *
18263 * @author Vova Feldman (@svovaf)
18264 * @since 1.0.9
18265 *
18266 * @param bool|number $plugin_id
18267 *
18268 * @return string
18269 */
18270 private function get_latest_download_api_url( $plugin_id = false ) {
18271 $this->_logger->entrance();
18272
18273 return $this->get_api_site_scope()->get_signed_url(
18274 $this->_get_latest_version_endpoint( $plugin_id, 'zip' )
18275 );
18276 }
18277
18278 /**
18279 * Get payment invoice URL.
18280 *
18281 * @author Vova Feldman (@svovaf)
18282 * @since 1.2.0
18283 *
18284 * @param bool|number $payment_id
18285 *
18286 * @return string
18287 */
18288 function _get_invoice_api_url( $payment_id = false ) {
18289 $this->_logger->entrance();
18290
18291 return $this->get_api_user_scope()->get_signed_url(
18292 "/payments/{$payment_id}/invoice.pdf"
18293 );
18294 }
18295
18296 /**
18297 * Get latest plugin download link.
18298 *
18299 * @author Vova Feldman (@svovaf)
18300 * @since 1.0.9
18301 *
18302 * @param string $label
18303 * @param bool|number $plugin_id
18304 *
18305 * @return string
18306 */
18307 private function get_latest_download_link( $label, $plugin_id = false ) {
18308 return sprintf(
18309 '<a target="_blank" href="%s">%s</a>',
18310 $this->_get_latest_download_local_url( $plugin_id ),
18311 $label
18312 );
18313 }
18314
18315 /**
18316 * Get latest plugin download local URL.
18317 *
18318 * @author Vova Feldman (@svovaf)
18319 * @since 1.0.9
18320 *
18321 * @param bool|number $plugin_id
18322 *
18323 * @return string
18324 */
18325 function _get_latest_download_local_url( $plugin_id = false ) {
18326 // Add timestamp to protect from caching.
18327 $params = array( 'ts' => WP_FS__SCRIPT_START_TIME );
18328
18329 if ( ! empty( $plugin_id ) ) {
18330 $params['plugin_id'] = $plugin_id;
18331 } else if ( $this->is_addon() ) {
18332 $params['plugin_id'] = $this->get_id();
18333 }
18334
18335 $fs = $this->is_addon() ?
18336 $this->get_parent_instance() :
18337 $this;
18338
18339 return $fs->get_account_url( 'download_latest', $params );
18340 }
18341
18342 #endregion Download Plugin ------------------------------------------------------------------
18343
18344 /**
18345 * @author Vova Feldman (@svovaf)
18346 * @since 1.0.4
18347 *
18348 * @uses FS_Api
18349 *
18350 * @param bool $background Hints the method if it's a background updates check. If false, it means that
18351 * was initiated by the admin.
18352 * @param bool|number $plugin_id
18353 * @param bool $flush Since 1.1.7.3
18354 * @param int $expiration Since 1.2.2.7
18355 * @param bool|string $newer_than Since 2.2.1
18356 */
18357 private function check_updates(
18358 $background = false,
18359 $plugin_id = false,
18360 $flush = true,
18361 $expiration = WP_FS__TIME_24_HOURS_IN_SEC,
18362 $newer_than = false
18363 ) {
18364 $this->_logger->entrance();
18365
18366 // Check if there's a newer version for download.
18367 $new_version = $this->_fetch_newer_version( $plugin_id, $flush, $expiration, $newer_than );
18368
18369 $update = null;
18370 if ( is_object( $new_version ) ) {
18371 $update = new FS_Plugin_Tag( $new_version );
18372
18373 if ( ! $background ) {
18374 $this->_admin_notices->add(
18375 sprintf(
18376 /* translators: %s: Numeric version number (e.g. '2.1.9' */
18377 $this->get_text_inline( 'Version %s was released.', 'version-x-released' ) . ' ' . $this->get_text_inline( 'Please download %s.', 'please-download-x' ),
18378 $update->version,
18379 sprintf(
18380 '<a href="%s" target="_blank">%s</a>',
18381 $this->get_account_url( 'download_latest' ),
18382 sprintf(
18383 /* translators: %s: plan name (e.g. latest "Professional" version) */
18384 $this->get_text_inline( 'the latest %s version here', 'latest-x-version' ),
18385 $this->get_plan_title()
18386 )
18387 )
18388 ),
18389 $this->get_text_inline( 'New', 'new' ) . '!'
18390 );
18391 }
18392 } else if ( false === $new_version && ! $background ) {
18393 $this->_admin_notices->add(
18394 $this->get_text_inline( 'Seems like you got the latest release.', 'you-have-latest' ),
18395 $this->get_text_inline( 'You are all good!', 'you-are-good' )
18396 );
18397 }
18398
18399 $this->_store_update( $update, true, $plugin_id );
18400 }
18401
18402 /**
18403 * @author Vova Feldman (@svovaf)
18404 * @since 1.0.4
18405 *
18406 * @param bool $flush Since 1.1.7.3 add 24 hour cache by default.
18407 *
18408 * @return FS_Plugin[]
18409 *
18410 * @uses FS_Api
18411 */
18412 private function sync_addons( $flush = false ) {
18413 $this->_logger->entrance();
18414
18415 $api = $this->get_api_site_or_plugin_scope();
18416
18417 $path = $this->add_show_pending( '/addons.json?enriched=true' );
18418
18419 /**
18420 * @since 1.2.1
18421 *
18422 * If there's a cached version of the add-ons and not asking
18423 * for a flush, just use the currently stored add-ons.
18424 */
18425 if ( ! $flush && $api->is_cached( $path ) ) {
18426 $addons = self::get_all_addons();
18427
18428 return $addons[ $this->_plugin->id ];
18429 }
18430
18431 $result = $api->get( $path, $flush );
18432
18433 $addons = array();
18434 if ( $this->is_api_result_object( $result, 'plugins' ) &&
18435 is_array( $result->plugins )
18436 ) {
18437 for ( $i = 0, $len = count( $result->plugins ); $i < $len; $i ++ ) {
18438 $addons[ $i ] = new FS_Plugin( $result->plugins[ $i ] );
18439 }
18440
18441 $this->_store_addons( $addons, true );
18442 }
18443
18444 return $addons;
18445 }
18446
18447 /**
18448 * Handle user email update.
18449 *
18450 * @author Vova Feldman (@svovaf)
18451 * @since 1.0.3
18452 * @uses FS_Api
18453 *
18454 * @param string $new_email
18455 *
18456 * @return object
18457 */
18458 private function update_email( $new_email ) {
18459 $this->_logger->entrance();
18460
18461
18462 $api = $this->get_api_user_scope();
18463 $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,email,is_verified", 'put', array(
18464 'email' => $new_email,
18465 'after_email_confirm_url' => $this->_get_admin_page_url(
18466 'account',
18467 array( 'fs_action' => 'sync_user' )
18468 ),
18469 ) );
18470
18471 if ( ! isset( $user->error ) ) {
18472 $this->_user->email = $user->email;
18473 $this->_user->is_verified = $user->is_verified;
18474 $this->_store_user();
18475 } else {
18476 // handle different error cases.
18477
18478 }
18479
18480 return $user;
18481 }
18482
18483 #----------------------------------------------------------------------------------
18484 #region API Error Handling
18485 #----------------------------------------------------------------------------------
18486
18487 /**
18488 * @author Vova Feldman (@svovaf)
18489 * @since 1.1.1
18490 *
18491 * @param mixed $result
18492 *
18493 * @return bool Is API result contains an error.
18494 */
18495 private function is_api_error( $result ) {
18496 return FS_Api::is_api_error( $result );
18497 }
18498
18499 /**
18500 * Checks if given API result is a non-empty and not an error object.
18501 *
18502 * @author Vova Feldman (@svovaf)
18503 * @since 1.2.1.5
18504 *
18505 * @param mixed $result
18506 * @param string|null $required_property Optional property we want to verify that is set.
18507 *
18508 * @return bool
18509 */
18510 function is_api_result_object( $result, $required_property = null ) {
18511 return FS_Api::is_api_result_object( $result, $required_property );
18512 }
18513
18514 /**
18515 * Checks if given API result is a non-empty entity object with non-empty ID.
18516 *
18517 * @author Vova Feldman (@svovaf)
18518 * @since 1.2.1.5
18519 *
18520 * @param mixed $result
18521 *
18522 * @return bool
18523 */
18524 private function is_api_result_entity( $result ) {
18525 return FS_Api::is_api_result_entity( $result );
18526 }
18527
18528 #endregion
18529
18530 /**
18531 * Make sure a given argument is an array of a specific type.
18532 *
18533 * @author Vova Feldman (@svovaf)
18534 * @since 1.2.1.5
18535 *
18536 * @param mixed $array
18537 * @param string $class
18538 *
18539 * @return bool
18540 */
18541 private function is_array_instanceof( $array, $class ) {
18542 return ( is_array( $array ) && ( empty( $array ) || $array[0] instanceof $class ) );
18543 }
18544
18545 /**
18546 * Start install ownership change.
18547 *
18548 * @author Vova Feldman (@svovaf)
18549 * @since 1.1.1
18550 * @uses FS_Api
18551 *
18552 * @param string $candidate_email
18553 *
18554 * @return bool Is ownership change successfully initiated.
18555 */
18556 private function init_change_owner( $candidate_email ) {
18557 $this->_logger->entrance();
18558
18559 $api = $this->get_api_site_scope();
18560 $result = $api->call( "/users/{$this->_user->id}.json", 'put', array(
18561 'email' => $candidate_email,
18562 'after_confirm_url' => $this->_get_admin_page_url(
18563 'account',
18564 array( 'fs_action' => 'change_owner' )
18565 ),
18566 ) );
18567
18568 return ! $this->is_api_error( $result );
18569 }
18570
18571 /**
18572 * Handle install ownership change.
18573 *
18574 * @author Vova Feldman (@svovaf)
18575 * @since 1.1.1
18576 * @uses FS_Api
18577 *
18578 * @return bool Was ownership change successfully complete.
18579 */
18580 private function complete_change_owner() {
18581 $this->_logger->entrance();
18582
18583 $site_result = $this->get_api_site_scope( true )->get();
18584 $site = new FS_Site( $site_result );
18585 $this->_site = $site;
18586
18587 $user = new FS_User();
18588 $user->id = fs_request_get( 'user_id' );
18589
18590 // Validate install's user and given user.
18591 if ( $user->id != $this->_site->user_id ) {
18592 return false;
18593 }
18594
18595 $user->public_key = fs_request_get( 'user_public_key' );
18596 $user->secret_key = fs_request_get( 'user_secret_key' );
18597
18598 // Fetch new user information.
18599 $this->_user = $user;
18600 $user_result = $this->get_api_user_scope( true )->get();
18601 $user = new FS_User( $user_result );
18602 $this->_user = $user;
18603
18604 $this->_set_account( $user, $site );
18605
18606 return true;
18607 }
18608
18609 /**
18610 * Handle user name update.
18611 *
18612 * @author Vova Feldman (@svovaf)
18613 * @since 1.0.9
18614 * @uses FS_Api
18615 *
18616 * @return object
18617 */
18618 private function update_user_name() {
18619 $this->_logger->entrance();
18620 $name = fs_request_get( 'fs_user_name_' . $this->get_unique_affix(), '' );
18621
18622 $api = $this->get_api_user_scope();
18623 $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,first,last", 'put', array(
18624 'name' => $name,
18625 ) );
18626
18627 if ( ! isset( $user->error ) ) {
18628 $this->_user->first = $user->first;
18629 $this->_user->last = $user->last;
18630 $this->_store_user();
18631 } else {
18632 // handle different error cases.
18633
18634 }
18635
18636 return $user;
18637 }
18638
18639 /**
18640 * Verify user email.
18641 *
18642 * @author Vova Feldman (@svovaf)
18643 * @since 1.0.3
18644 * @uses FS_Api
18645 */
18646 private function verify_email() {
18647 $this->_handle_account_user_sync();
18648
18649 if ( $this->_user->is_verified() ) {
18650 return;
18651 }
18652
18653 $api = $this->get_api_site_scope();
18654 $result = $api->call( "/users/{$this->_user->id}/verify.json", 'put', array(
18655 'after_email_confirm_url' => $this->_get_admin_page_url(
18656 'account',
18657 array( 'fs_action' => 'sync_user' )
18658 )
18659 ) );
18660
18661 if ( ! isset( $result->error ) ) {
18662 $this->_admin_notices->add( sprintf(
18663 $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' ),
18664 sprintf( '<a href="mailto:%1s">%2s</a>', esc_url( $this->_user->email ), $this->_user->email )
18665 ) );
18666 } else {
18667 // handle different error cases.
18668
18669 }
18670 }
18671
18672 /**
18673 * @author Vova Feldman (@svovaf)
18674 * @since 1.1.2
18675 *
18676 * @param array $params
18677 * @param bool|null $network
18678 *
18679 * @return string
18680 */
18681 function get_activation_url( $params = array(), $network = null ) {
18682 if ( $this->is_addon() && $this->has_free_plan() ) {
18683 /**
18684 * @author Vova Feldman (@svovaf)
18685 * @since 1.2.1.7 Add-on's activation is the parent's module activation.
18686 */
18687 return $this->get_parent_instance()->get_activation_url( $params );
18688 }
18689
18690 return $this->apply_filters( 'connect_url', $this->_get_admin_page_url( '', $params, $network ) );
18691 }
18692
18693 /**
18694 * @author Vova Feldman (@svovaf)
18695 * @since 1.2.1.5
18696 *
18697 * @param array $params
18698 *
18699 * @return string
18700 */
18701 function get_reconnect_url( $params = array() ) {
18702 $params['fs_action'] = 'reset_anonymous_mode';
18703 $params['fs_unique_affix'] = $this->get_unique_affix();
18704
18705 return $this->get_activation_url( $params );
18706 }
18707
18708 /**
18709 * Get the URL of the page that should be loaded after the user connect
18710 * or skip in the opt-in screen.
18711 *
18712 * @author Vova Feldman (@svovaf)
18713 * @since 1.1.3
18714 *
18715 * @param string $filter Filter name.
18716 * @param array $params Since 1.2.2.7
18717 * @param bool|null $network
18718 *
18719 * @return string
18720 */
18721 function get_after_activation_url( $filter, $params = array(), $network = null ) {
18722 if ( $this->is_free_wp_org_theme() &&
18723 ( fs_request_has( 'pending_activation' ) ||
18724 // For cases when the first time path is set, even though it's a WP.org theme.
18725 fs_request_get_bool( $this->get_unique_affix() . '_show_optin' ) )
18726 ) {
18727 $first_time_path = '';
18728 } else {
18729 $first_time_path = $this->_menu->get_first_time_path();
18730 }
18731
18732 if ( $this->_is_network_active &&
18733 fs_is_network_admin() &&
18734 ! $this->_menu->has_network_menu() &&
18735 $this->is_network_registered()
18736 ) {
18737 $target_url = $this->get_account_url();
18738 } else {
18739 // Default plugin's page.
18740 $target_url = $this->_get_admin_page_url( '', array(), $network );
18741 }
18742
18743 return add_query_arg( $params, $this->apply_filters(
18744 $filter,
18745 empty( $first_time_path ) ?
18746 $target_url :
18747 $first_time_path
18748 ) );
18749 }
18750
18751 /**
18752 * Handle account page updates / edits / actions.
18753 *
18754 * @author Vova Feldman (@svovaf)
18755 * @since 1.0.2
18756 *
18757 */
18758 private function _handle_account_edits() {
18759 if ( ! $this->is_user_admin() ) {
18760 return;
18761 }
18762
18763 $action = fs_get_action();
18764
18765 if ( empty( $action ) ) {
18766 return;
18767 }
18768
18769 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
18770 $install_id = fs_request_get( 'install_id', '' );
18771
18772 // Alias.
18773 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
18774
18775 $is_network_action = $this->is_network_level_action();
18776 $blog_id = $this->is_network_level_site_specific_action();
18777
18778 if ( is_numeric( $blog_id ) ) {
18779 $this->switch_to_blog( $blog_id );
18780 } else {
18781 $blog_id = '';
18782 }
18783
18784 switch ( $action ) {
18785 case 'opt_in':
18786 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
18787
18788 if ( $plugin_id == $this->get_id() ) {
18789 if ( $is_network_action && ! empty( $blog_id ) ) {
18790 if ( ! $this->is_registered() ) {
18791 $this->install_with_user(
18792 $this->get_network_user(),
18793 false,
18794 false,
18795 false,
18796 false
18797 );
18798
18799 $this->_admin_notices->add(
18800 $this->get_text_inline( 'Site successfully opted in.', 'successful-opt-in' ),
18801 $this->get_text_inline( 'Awesome', 'awesome' )
18802 );
18803 }
18804 }
18805 }
18806 break;
18807
18808 case 'toggle_tracking':
18809 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
18810
18811 if ( $plugin_id == $this->get_id() ) {
18812 if ( $is_network_action && ! empty( $blog_id ) ) {
18813 if ( $this->is_registered() ) {
18814 if ( $this->is_tracking_prohibited() ) {
18815 if ( $this->allow_site_tracking() ) {
18816 $this->_admin_notices->add(
18817 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 ),
18818 $this->get_text_inline( 'Thank you!', 'thank-you' )
18819 );
18820 }
18821 } else {
18822 if ( $this->stop_site_tracking() ) {
18823 $this->_admin_notices->add(
18824 sprintf(
18825 $this->get_text_inline( 'We will no longer be sending any usage data of %s on %s to %s.', 'opted-out-successfully' ),
18826 $this->get_plugin_title(),
18827 fs_strip_url_protocol( get_site_url( $blog_id ) ),
18828 sprintf(
18829 '<a href="%s" target="_blank">%s</a>',
18830 'https://freemius.com',
18831 'freemius.com'
18832 )
18833 )
18834 );
18835 }
18836 }
18837 }
18838 }
18839 }
18840
18841 break;
18842
18843 case 'delete_account':
18844 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
18845
18846 if ( $plugin_id == $this->get_id() ) {
18847 if ( $is_network_action && empty( $blog_id ) ) {
18848 $this->delete_network_account_event();
18849 } else {
18850 $this->delete_account_event();
18851 }
18852
18853 // Clear user and site.
18854 $this->_site = null;
18855 $this->_user = null;
18856
18857 $this->maybe_set_slug_and_network_menu_exists_flag();
18858
18859 fs_redirect( $this->get_activation_url() );
18860 } else {
18861 if ( $this->is_addon_activated( $plugin_id ) ) {
18862 $fs_addon = self::get_instance_by_id( $plugin_id );
18863 $fs_addon->delete_account_event();
18864
18865 fs_redirect( $this->_get_admin_page_url( 'account' ) );
18866 }
18867 }
18868
18869 return;
18870
18871 case 'downgrade_account':
18872 if ( is_numeric( $blog_id ) ) {
18873 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
18874 } else {
18875 check_admin_referer( $action );
18876 }
18877
18878 $switch_to_network_install_blog_after_cancellation = (
18879 is_numeric( $blog_id ) &&
18880 $plugin_id == $this->get_id() &&
18881 ! $this->is_trial()
18882 );
18883
18884 $result = $this->cancel_subscription_or_trial( $plugin_id );
18885 if ( $this->is_api_error( $result ) ) {
18886 $this->_admin_notices->add(
18887 $result->error->message,
18888 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
18889 'error'
18890 );
18891 }
18892
18893 if ( $switch_to_network_install_blog_after_cancellation ) {
18894 $this->switch_to_blog( $this->_storage->network_install_blog_id );
18895 }
18896
18897 return;
18898
18899 case 'activate_license':
18900 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
18901
18902 $fs = $this;
18903 if ( $plugin_id != $this->get_id() ) {
18904 $fs = $this->is_addon_activated( $plugin_id ) ?
18905 self::get_instance_by_id( $plugin_id ) :
18906 null;
18907 }
18908
18909 if ( is_object( $fs ) ) {
18910 $fs->_activate_license();
18911 }
18912
18913 return;
18914
18915 case 'deactivate_license':
18916 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
18917
18918 if ( $plugin_id == $this->get_id() ) {
18919 $this->_deactivate_license();
18920
18921 if ( $this->is_only_premium() ) {
18922 // Clear user and site.
18923 $this->_site = null;
18924 $this->_user = null;
18925
18926 if ( ! $is_network_action ) {
18927 fs_redirect( $this->get_activation_url() );
18928 } else if ( is_numeric( $blog_id ) ) {
18929 $this->switch_to_blog( $this->_storage->network_install_blog_id );
18930 }
18931 }
18932 } else {
18933 if ( $this->is_addon_activated( $plugin_id ) ) {
18934 $fs_addon = self::get_instance_by_id( $plugin_id );
18935 $fs_addon->_deactivate_license();
18936 }
18937 }
18938
18939 return;
18940
18941 case 'check_updates':
18942 check_admin_referer( $action );
18943 $this->check_updates();
18944
18945 return;
18946
18947 case 'change_owner':
18948 $state = fs_request_get( 'state', 'init' );
18949 switch ( $state ) {
18950 case 'init':
18951 $candidate_email = fs_request_get( 'candidate_email', '' );
18952
18953 if ( $this->init_change_owner( $candidate_email ) ) {
18954 $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>' ) );
18955 }
18956 break;
18957 case 'owner_confirmed':
18958 $candidate_email = fs_request_get( 'candidate_email', '' );
18959
18960 $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>' ) );
18961 break;
18962 case 'candidate_confirmed':
18963 if ( $this->complete_change_owner() ) {
18964 $this->_admin_notices->add_sticky(
18965 sprintf( $this->get_text_inline( '%s is the new owner of the account.', 'change-owner-request_candidate-confirmed' ), '<b>' . $this->_user->email . '</b>' ),
18966 'ownership_changed',
18967 $this->get_text_x_inline( 'Congrats', 'as congratulations', 'congrats' ) . '!'
18968 );
18969 } else {
18970 // @todo Handle failed ownership change message.
18971 }
18972 break;
18973 }
18974
18975 return;
18976
18977 case 'update_email':
18978 check_admin_referer( 'update_email' );
18979
18980 $new_email = fs_request_get( 'fs_email_' . $this->get_unique_affix(), '' );
18981 $result = $this->update_email( $new_email );
18982
18983 if ( isset( $result->error ) ) {
18984 switch ( $result->error->code ) {
18985 case 'user_exist':
18986 $this->_admin_notices->add(
18987 $this->get_text_inline( 'Sorry, we could not complete the email update. Another user with the same email is already registered.', 'user-exist-message' ) . ' ' .
18988 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>' ) .
18989 sprintf(
18990 '<a style="margin-left: 10px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
18991 $this->get_account_url( 'change_owner', array(
18992 'state' => 'init',
18993 'candidate_email' => $new_email
18994 ) ),
18995 $this->get_text_inline( 'Change Ownership', 'change-ownership' )
18996 ),
18997 $oops_text,
18998 'error'
18999 );
19000 break;
19001 }
19002 } else {
19003 $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' ) );
19004 }
19005
19006 return;
19007
19008 case 'update_user_name':
19009 check_admin_referer( 'update_user_name' );
19010
19011 $result = $this->update_user_name();
19012
19013 if ( isset( $result->error ) ) {
19014 $this->_admin_notices->add(
19015 $this->get_text_inline( 'Please provide your full name.', 'name-update-failed-message' ),
19016 $oops_text,
19017 'error'
19018 );
19019 } else {
19020 $this->_admin_notices->add( $this->get_text_inline( 'Your name was successfully updated.', 'name-updated-message' ) );
19021 }
19022
19023 return;
19024
19025 #region Actions that might be called from external links (e.g. email)
19026
19027 case 'cancel_trial':
19028 $result = $this->cancel_subscription_or_trial( $plugin_id );
19029 if ( $this->is_api_error( $result ) ) {
19030 $this->_admin_notices->add(
19031 $result->error->message,
19032 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
19033 'error'
19034 );
19035 }
19036
19037 return;
19038
19039 case 'verify_email':
19040 $this->verify_email();
19041
19042 return;
19043
19044 case 'sync_user':
19045 $this->_handle_account_user_sync();
19046
19047 return;
19048
19049 case $this->get_unique_affix() . '_sync_license':
19050 $this->_sync_license();
19051
19052 return;
19053
19054 case 'download_latest':
19055 $this->download_latest_directly( $plugin_id );
19056
19057 return;
19058
19059 #endregion
19060 }
19061
19062 if ( WP_FS__IS_POST_REQUEST ) {
19063 $properties = array( 'site_secret_key', 'site_id', 'site_public_key' );
19064 foreach ( $properties as $p ) {
19065 if ( 'update_' . $p === $action ) {
19066 check_admin_referer( $action );
19067
19068 $this->_logger->log( $action );
19069
19070 $site_property = substr( $p, strlen( 'site_' ) );
19071 $site_property_value = fs_request_get( 'fs_' . $p . '_' . $this->get_unique_affix(), '' );
19072 $this->get_site()->{$site_property} = $site_property_value;
19073
19074 // Store account after modification.
19075 $this->_store_site();
19076
19077 $this->do_action( 'account_property_edit', 'site', $site_property, $site_property_value );
19078
19079 $this->_admin_notices->add( sprintf(
19080 /* translators: %s: User's account property (e.g. email address, name) */
19081 $this->get_text_inline( 'You have successfully updated your %s.', 'x-updated' ),
19082 '<b>' . str_replace( '_', ' ', $p ) . '</b>'
19083 ) );
19084
19085 return;
19086 }
19087 }
19088 }
19089 }
19090
19091 /**
19092 * Account page resources load.
19093 *
19094 * @author Vova Feldman (@svovaf)
19095 * @since 1.0.6
19096 */
19097 function _account_page_load() {
19098 $this->_logger->entrance();
19099
19100 $this->_logger->info( var_export( $_REQUEST, true ) );
19101
19102 fs_enqueue_local_style( 'fs_account', '/admin/account.css' );
19103
19104 if ( $this->has_addons() ) {
19105 wp_enqueue_script( 'plugin-install' );
19106 add_thickbox();
19107
19108 function fs_addons_body_class( $classes ) {
19109 $classes .= ' plugins-php';
19110
19111 return $classes;
19112 }
19113
19114 add_filter( 'admin_body_class', 'fs_addons_body_class' );
19115 }
19116
19117 if ( $this->has_paid_plan() &&
19118 ! $this->has_any_license() &&
19119 ! $this->is_sync_executed() &&
19120 $this->is_tracking_allowed()
19121 ) {
19122 /**
19123 * If no licenses found and no sync job was executed during the last 24 hours,
19124 * just execute the sync job right away (blocking execution).
19125 *
19126 * @since 1.1.7.3
19127 */
19128 $this->run_manual_sync();
19129 }
19130
19131 $this->_handle_account_edits();
19132
19133 $this->do_action( 'account_page_load_before_departure' );
19134 }
19135
19136 /**
19137 * Renders the "Affiliation" page.
19138 *
19139 * @author Leo Fajardo (@leorw)
19140 * @since 1.2.3
19141 */
19142 function _affiliation_page_render() {
19143 $this->_logger->entrance();
19144
19145 $this->fetch_affiliate_and_terms();
19146
19147 fs_enqueue_local_style( 'fs_affiliation', '/admin/affiliation.css' );
19148
19149 $vars = array( 'id' => $this->_module_id );
19150 echo $this->apply_filters( "/forms/affiliation.php", fs_get_template( '/forms/affiliation.php', $vars ) );
19151 }
19152
19153
19154 /**
19155 * Render account page.
19156 *
19157 * @author Vova Feldman (@svovaf)
19158 * @since 1.0.0
19159 */
19160 function _account_page_render() {
19161 $this->_logger->entrance();
19162
19163 $template = 'account.php';
19164 $vars = array( 'id' => $this->_module_id );
19165
19166 /**
19167 * Added filter to the template to allow developers wrapping the template
19168 * in custom HTML (e.g. within a wizard/tabs).
19169 *
19170 * @author Vova Feldman (@svovaf)
19171 * @since 1.2.1.6
19172 */
19173 echo $this->apply_filters( "templates/{$template}", fs_get_template( $template, $vars ) );
19174 }
19175
19176 /**
19177 * Render account connect page.
19178 *
19179 * @author Vova Feldman (@svovaf)
19180 * @since 1.0.7
19181 */
19182 function _connect_page_render() {
19183 $this->_logger->entrance();
19184
19185 $vars = array( 'id' => $this->_module_id );
19186
19187 /**
19188 * Added filter to the template to allow developers wrapping the template
19189 * in custom HTML (e.g. within a wizard/tabs).
19190 *
19191 * @author Vova Feldman (@svovaf)
19192 * @since 1.2.1.6
19193 */
19194 echo $this->apply_filters( 'templates/connect.php', fs_get_template( 'connect.php', $vars ) );
19195 }
19196
19197 /**
19198 * Load required resources before add-ons page render.
19199 *
19200 * @author Vova Feldman (@svovaf)
19201 * @since 1.0.6
19202 */
19203 function _addons_page_load() {
19204 $this->_logger->entrance();
19205
19206 fs_enqueue_local_style( 'fs_addons', '/admin/add-ons.css' );
19207
19208 wp_enqueue_script( 'plugin-install' );
19209 add_thickbox();
19210
19211 function fs_addons_body_class( $classes ) {
19212 $classes .= ' plugins-php';
19213
19214 return $classes;
19215 }
19216
19217 add_filter( 'admin_body_class', 'fs_addons_body_class' );
19218
19219 if ( ! $this->is_registered() && $this->is_org_repo_compliant() ) {
19220 $this->_admin_notices->add(
19221 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>' ),
19222 $this->get_text_x_inline( 'Heads up', 'advance notice of something that will need attention.', 'heads-up' ),
19223 'update-nag'
19224 );
19225 }
19226 }
19227
19228 /**
19229 * Render add-ons page.
19230 *
19231 * @author Vova Feldman (@svovaf)
19232 * @since 1.0.6
19233 */
19234 function _addons_page_render() {
19235 $this->_logger->entrance();
19236
19237 $vars = array( 'id' => $this->_module_id );
19238
19239 /**
19240 * Added filter to the template to allow developers wrapping the template
19241 * in custom HTML (e.g. within a wizard/tabs).
19242 *
19243 * @author Vova Feldman (@svovaf)
19244 * @since 1.2.1.6
19245 */
19246 echo $this->apply_filters( 'templates/add-ons.php', fs_get_template( 'add-ons.php', $vars ) );
19247 }
19248
19249 /* Pricing & Upgrade
19250 ------------------------------------------------------------------------------------------------------------------*/
19251 /**
19252 * Render pricing page.
19253 *
19254 * @author Vova Feldman (@svovaf)
19255 * @since 1.0.0
19256 */
19257 function _pricing_page_render() {
19258 $this->_logger->entrance();
19259
19260 $vars = array( 'id' => $this->_module_id );
19261
19262 if ( 'true' === fs_request_get( 'checkout', false ) ) {
19263 echo $this->apply_filters( 'templates/checkout.php', fs_get_template( 'checkout.php', $vars ) );
19264 } else {
19265 echo $this->apply_filters( 'templates/pricing.php', fs_get_template( 'pricing.php', $vars ) );
19266 }
19267 }
19268
19269 #----------------------------------------------------------------------------------
19270 #region Contact Us
19271 #----------------------------------------------------------------------------------
19272
19273 /**
19274 * Render contact-us page.
19275 *
19276 * @author Vova Feldman (@svovaf)
19277 * @since 1.0.3
19278 */
19279 function _contact_page_render() {
19280 $this->_logger->entrance();
19281
19282 $vars = array( 'id' => $this->_module_id );
19283
19284 /**
19285 * Added filter to the template to allow developers wrapping the template
19286 * in custom HTML (e.g. within a wizard/tabs).
19287 *
19288 * @author Vova Feldman (@svovaf)
19289 * @since 2.1.3
19290 */
19291 echo $this->apply_filters( 'templates/contact.php', fs_get_template( 'contact.php', $vars ) );
19292 }
19293
19294 #endregion ------------------------------------------------------------------------
19295
19296 /**
19297 * Hide all admin notices to prevent distractions.
19298 *
19299 * @author Vova Feldman (@svovaf)
19300 * @since 1.0.3
19301 *
19302 * @uses remove_all_actions()
19303 */
19304 private static function _hide_admin_notices() {
19305 remove_all_actions( 'admin_notices' );
19306 remove_all_actions( 'network_admin_notices' );
19307 remove_all_actions( 'all_admin_notices' );
19308 remove_all_actions( 'user_admin_notices' );
19309 }
19310
19311 static function _clean_admin_content_section_hook() {
19312 self::_hide_admin_notices();
19313
19314 // Hide footer.
19315 echo '<style>#wpfooter { display: none !important; }</style>';
19316 }
19317
19318 /**
19319 * Attach to admin_head hook to hide all admin notices.
19320 *
19321 * @author Vova Feldman (@svovaf)
19322 * @since 1.0.3
19323 */
19324 static function _clean_admin_content_section() {
19325 add_action( 'admin_head', 'Freemius::_clean_admin_content_section_hook' );
19326 }
19327
19328 /* CSS & JavaScript
19329 ------------------------------------------------------------------------------------------------------------------*/
19330 /* function _enqueue_script($handle, $src) {
19331 $url = plugins_url( substr( WP_FS__DIR_JS, strlen( $this->_plugin_dir_path ) ) . '/assets/js/' . $src );
19332
19333 $this->_logger->entrance( 'script = ' . $url );
19334
19335 wp_enqueue_script( $handle, $url );
19336 }*/
19337
19338 /* SDK
19339 ------------------------------------------------------------------------------------------------------------------*/
19340 private $_user_api;
19341
19342 /**
19343 *
19344 * @author Vova Feldman (@svovaf)
19345 * @since 1.0.2
19346 *
19347 * @param bool $flush
19348 *
19349 * @return FS_Api
19350 */
19351 private function get_api_user_scope( $flush = false ) {
19352 if ( ! isset( $this->_user_api ) || $flush ) {
19353 $this->_user_api = $this->get_api_user_scope_by_user( $this->_user );
19354 }
19355
19356 return $this->_user_api;
19357 }
19358
19359 /**
19360 * @author Vova Feldman (@svovaf)
19361 * @since 2.0.0
19362 *
19363 * @param \FS_User $user
19364 *
19365 * @return \FS_Api
19366 */
19367 private function get_api_user_scope_by_user( FS_User $user ) {
19368 return FS_Api::instance(
19369 $this->_module_id,
19370 'user',
19371 $user->id,
19372 $user->public_key,
19373 ! $this->is_live(),
19374 $user->secret_key
19375 );
19376 }
19377
19378 /**
19379 *
19380 * @author Leo Fajardo (@leorw)
19381 * @since 2.0.0
19382 *
19383 * @param bool $flush
19384 *
19385 * @return FS_Api
19386 */
19387 private function get_current_or_network_user_api_scope( $flush = false ) {
19388 if ( ! $this->_is_network_active ||
19389 ( isset( $this->_user ) && $this->_user instanceof FS_User )
19390 ) {
19391 return $this->get_api_user_scope( $flush );
19392 }
19393
19394 $user = $this->get_current_or_network_user();
19395
19396 $this->_user_api = FS_Api::instance(
19397 $this->_module_id,
19398 'user',
19399 $user->id,
19400 $user->public_key,
19401 ! $this->is_live(),
19402 $user->secret_key
19403 );
19404
19405 return $this->_user_api;
19406 }
19407
19408 private $_site_api;
19409
19410 /**
19411 *
19412 * @author Vova Feldman (@svovaf)
19413 * @since 1.0.2
19414 *
19415 * @param bool $flush
19416 *
19417 * @return FS_Api
19418 */
19419 private function get_api_site_scope( $flush = false ) {
19420 if ( ! isset( $this->_site_api ) || $flush ) {
19421 $this->_site_api = FS_Api::instance(
19422 $this->_module_id,
19423 'install',
19424 $this->_site->id,
19425 $this->_site->public_key,
19426 ! $this->is_live(),
19427 $this->_site->secret_key
19428 );
19429 }
19430
19431 return $this->_site_api;
19432 }
19433
19434 private $_plugin_api;
19435
19436 /**
19437 * Get plugin public API scope.
19438 *
19439 * @author Vova Feldman (@svovaf)
19440 * @since 1.0.7
19441 *
19442 * @return FS_Api
19443 */
19444 function get_api_plugin_scope() {
19445 if ( ! isset( $this->_plugin_api ) ) {
19446 $this->_plugin_api = FS_Api::instance(
19447 $this->_module_id,
19448 'plugin',
19449 $this->_plugin->id,
19450 $this->_plugin->public_key,
19451 ! $this->is_live()
19452 );
19453 }
19454
19455 return $this->_plugin_api;
19456 }
19457
19458 /**
19459 * Get site API scope object (fallback to public plugin scope when not registered).
19460 *
19461 * @author Vova Feldman (@svovaf)
19462 * @since 1.0.7
19463 *
19464 * @return FS_Api
19465 */
19466 function get_api_site_or_plugin_scope() {
19467 return $this->is_registered() ?
19468 $this->get_api_site_scope() :
19469 $this->get_api_plugin_scope();
19470 }
19471
19472 /**
19473 * Show trial promotional notice (if any trial exist).
19474 *
19475 * @author Vova Feldman (@svovaf)
19476 * @since 1.0.9
19477 *
19478 * @param FS_Plugin_Plan[] $plans
19479 */
19480 function _check_for_trial_plans( $plans ) {
19481 /**
19482 * 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.
19483 *
19484 * @author Vova Feldman (@svovaf)
19485 * @since 2.1.2
19486 */
19487 if ( ! is_array( $plans ) && is_object( $plans ) ) {
19488 $plans = array( $plans );
19489 }
19490
19491 $this->_storage->has_trial_plan = FS_Plan_Manager::instance()->has_trial_plan( $plans );
19492 }
19493
19494 /**
19495 * During trial promotion the "upgrade" submenu item turns to
19496 * "start trial" to encourage the trial. Since we want to keep
19497 * the same menu item handler and there's no robust way to
19498 * add new arguments to the menu item link's querystring,
19499 * use JavaScript to find the menu item and update the href of
19500 * the link.
19501 *
19502 * @author Vova Feldman (@svovaf)
19503 * @since 1.2.1.5
19504 */
19505 function _fix_start_trial_menu_item_url() {
19506 $template_args = array( 'id' => $this->_module_id );
19507 fs_require_template( 'add-trial-to-pricing.php', $template_args );
19508 }
19509
19510 /**
19511 * Check if module is currently in a trial promotion mode.
19512 *
19513 * @author Vova Feldman (@svovaf)
19514 * @since 1.2.2.7
19515 *
19516 * @return bool
19517 */
19518 function is_in_trial_promotion() {
19519 return $this->_admin_notices->has_sticky( 'trial_promotion' );
19520 }
19521
19522 /**
19523 * Show trial promotional notice (if any trial exist).
19524 *
19525 * @author Vova Feldman (@svovaf)
19526 * @since 1.0.9
19527 *
19528 * @return bool If trial notice added.
19529 */
19530 function _add_trial_notice() {
19531 if ( ! $this->is_user_admin() ) {
19532 return false;
19533 }
19534
19535 if ( ! $this->is_user_in_admin() ) {
19536 return false;
19537 }
19538
19539 if ( $this->_is_network_active ) {
19540 if ( fs_is_network_admin() ) {
19541 // Network level trial is disabled at the moment.
19542 return false;
19543 }
19544
19545 if ( ! $this->is_delegated_connection() ) {
19546 // Only delegated sites should support trials.
19547 return false;
19548 }
19549 }
19550
19551 // Check if trial message is already shown.
19552 if ( $this->is_in_trial_promotion() ) {
19553 add_action( 'admin_footer', array( &$this, '_fix_start_trial_menu_item_url' ) );
19554
19555 $this->_menu->add_counter_to_menu_item( 1, 'fs-trial' );
19556
19557 return false;
19558 }
19559
19560 if ( $this->is_premium() && ! WP_FS__DEV_MODE ) {
19561 // Don't show trial if running the premium code, unless running in DEV mode.
19562 return false;
19563 }
19564
19565 if ( ! $this->has_trial_plan() ) {
19566 // No plans with trial.
19567 return false;
19568 }
19569
19570 if ( ! $this->apply_filters( 'show_trial', true ) ) {
19571 // Developer explicitly asked not to show the trial promo.
19572 return false;
19573 }
19574
19575 if ( $this->is_registered() ) {
19576 // Check if trial already utilized.
19577 if ( $this->_site->is_trial_utilized() ) {
19578 return false;
19579 }
19580
19581 if ( $this->is_paying_or_trial() ) {
19582 // Don't show trial if paying or already in trial.
19583 return false;
19584 }
19585 }
19586
19587 if ( $this->is_activation_mode() || $this->is_pending_activation() ) {
19588 // If not yet opted-in/skipped, or pending activation, don't show trial.
19589 return false;
19590 }
19591
19592 $last_time_trial_promotion_shown = $this->_storage->get( 'trial_promotion_shown', false );
19593 $was_promotion_shown_before = ( false !== $last_time_trial_promotion_shown );
19594
19595 // Show promotion if never shown before and 24 hours after initial activation with FS.
19596 if ( ! $was_promotion_shown_before &&
19597 $this->_storage->install_timestamp > ( time() - $this->apply_filters( 'show_first_trial_after_n_sec', WP_FS__TIME_24_HOURS_IN_SEC ) )
19598 ) {
19599 return false;
19600 }
19601
19602 // OR if promotion was shown before, try showing it every 30 days.
19603 if ( $was_promotion_shown_before &&
19604 $this->apply_filters( 'reshow_trial_after_every_n_sec', 30 * WP_FS__TIME_24_HOURS_IN_SEC ) > time() - $last_time_trial_promotion_shown
19605 ) {
19606 return false;
19607 }
19608
19609 $trial_period = $this->_trial_days;
19610 $require_payment = $this->_is_trial_require_payment;
19611 $trial_url = $this->get_trial_url();
19612 $plans_string = strtolower( $this->get_text_inline( 'Awesome', 'awesome' ) );
19613
19614 if ( $this->is_registered() ) {
19615 // If opted-in, override trial with up to date data from API.
19616 $trial_plans = FS_Plan_Manager::instance()->get_trial_plans( $this->_plans );
19617 $trial_plans_count = count( $trial_plans );
19618
19619 if ( 0 === $trial_plans_count ) {
19620 // If there's no plans with a trial just exit.
19621 return false;
19622 }
19623
19624 /**
19625 * @var FS_Plugin_Plan $paid_plan
19626 */
19627 $paid_plan = $trial_plans[0];
19628 $require_payment = $paid_plan->is_require_subscription;
19629 $trial_period = $paid_plan->trial_period;
19630
19631 $total_paid_plans = count( $this->_plans ) - ( FS_Plan_Manager::instance()->has_free_plan( $this->_plans ) ? 1 : 0 );
19632
19633 if ( $total_paid_plans !== $trial_plans_count ) {
19634 // Not all paid plans have a trial - generate a string of those that have it.
19635 for ( $i = 0; $i < $trial_plans_count; $i ++ ) {
19636 $plans_string .= sprintf(
19637 ' <a href="%s">%s</a>',
19638 $trial_url,
19639 $trial_plans[ $i ]->title
19640 );
19641
19642 if ( $i < $trial_plans_count - 2 ) {
19643 $plans_string .= ', ';
19644 } else if ( $i == $trial_plans_count - 2 ) {
19645 $plans_string .= ' and ';
19646 }
19647 }
19648 }
19649 }
19650
19651 $message = sprintf(
19652 $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' ),
19653 sprintf( '<b>%s</b>', $this->get_plugin_name() ),
19654 $plans_string,
19655 $trial_period
19656 );
19657
19658 // "No Credit-Card Required" or "No Commitment for N Days".
19659 $cc_string = $require_payment ?
19660 sprintf( $this->get_text_inline( 'No commitment for %s days - cancel anytime!', 'no-commitment-for-x-days' ), $trial_period ) :
19661 $this->get_text_inline( 'No credit card required', 'no-cc-required' ) . '!';
19662
19663
19664 // Start trial button.
19665 $button = ' ' . sprintf(
19666 '<a style="margin-left: 10px; vertical-align: super;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
19667 $trial_url,
19668 $this->get_text_x_inline( 'Start free trial', 'call to action', 'start-free-trial' )
19669 );
19670
19671 $this->_admin_notices->add_sticky(
19672 $this->apply_filters( 'trial_promotion_message', "{$message} {$cc_string} {$button}" ),
19673 'trial_promotion',
19674 '',
19675 'promotion'
19676 );
19677
19678 $this->_storage->trial_promotion_shown = WP_FS__SCRIPT_START_TIME;
19679
19680 return true;
19681 }
19682
19683 /**
19684 * Lets users/customers know that the product has an affiliate program.
19685 *
19686 * @author Leo Fajardo (@leorw)
19687 * @since 1.2.2.11
19688 *
19689 * @return bool Returns true if the notice has been added.
19690 */
19691 function _add_affiliate_program_notice() {
19692 if ( ! $this->is_user_admin() ) {
19693 return false;
19694 }
19695
19696 if ( ! $this->is_user_in_admin() ) {
19697 return false;
19698 }
19699
19700 // Check if the notice is already shown.
19701 if ( $this->_admin_notices->has_sticky( 'affiliate_program' ) ) {
19702 return false;
19703 }
19704
19705 if (
19706 // Product has no affiliate program.
19707 ! $this->has_affiliate_program() ||
19708 // User has applied for an affiliate account.
19709 ! empty( $this->_storage->affiliate_application_data )
19710 ) {
19711 return false;
19712 }
19713
19714 if ( ! $this->apply_filters( 'show_affiliate_program_notice', true ) ) {
19715 // Developer explicitly asked not to show the notice about the affiliate program.
19716 return false;
19717 }
19718
19719 if ( $this->is_activation_mode() || $this->is_pending_activation() ) {
19720 // If not yet opted in/skipped, or pending activation, don't show the notice.
19721 return false;
19722 }
19723
19724 $last_time_notice_was_shown = $this->_storage->get( 'affiliate_program_notice_shown', false );
19725 $was_notice_shown_before = ( false !== $last_time_notice_was_shown );
19726
19727 /**
19728 * Do not show the notice if it was already shown before or less than 30 days have passed since the initial
19729 * activation with FS.
19730 */
19731 if ( $was_notice_shown_before ||
19732 $this->_storage->install_timestamp > ( time() - ( WP_FS__TIME_24_HOURS_IN_SEC * 30 ) )
19733 ) {
19734 return false;
19735 }
19736
19737 if ( ! $this->is_paying() &&
19738 FS_Plugin::AFFILIATE_MODERATION_CUSTOMERS == $this->_plugin->affiliate_moderation
19739 ) {
19740 // If the user is not a customer and the affiliate program is only for customers, don't show the notice.
19741 return false;
19742 }
19743
19744 $message = sprintf(
19745 $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' ),
19746 sprintf( '<strong>%s</strong>', $this->get_plugin_name() ),
19747 $this->get_module_label( true )
19748 );
19749
19750 // HTML code for the "Learn more..." button.
19751 $button = ' ' . sprintf(
19752 '<a style="display: block; margin-top: 10px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
19753 $this->_get_admin_page_url( 'affiliation' ),
19754 $this->get_text_inline( 'Learn more', 'learn-more' ) . '...'
19755 );
19756
19757 $this->_admin_notices->add_sticky(
19758 $this->apply_filters( 'affiliate_program_notice', "{$message} {$button}" ),
19759 'affiliate_program',
19760 '',
19761 'promotion'
19762 );
19763
19764 $this->_storage->affiliate_program_notice_shown = WP_FS__SCRIPT_START_TIME;
19765
19766 return true;
19767 }
19768
19769 /**
19770 * @author Vova Feldman (@svovaf)
19771 * @since 1.2.1.5
19772 */
19773 function _enqueue_common_css() {
19774 if ( $this->has_paid_plan() && ! $this->is_paying() ) {
19775 // Add basic CSS for admin-notices and menu-item colors.
19776 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
19777 }
19778 }
19779
19780 /**
19781 * @author Leo Fajardo (@leorw)
19782 * @since 1.2.2
19783 */
19784 function _show_theme_activation_optin_dialog() {
19785 fs_enqueue_local_style( 'fs_connect', '/admin/connect.css' );
19786
19787 add_action( 'admin_footer-themes.php', array( &$this, '_add_fs_theme_activation_dialog' ) );
19788 }
19789
19790 /**
19791 * @author Leo Fajardo (@leorw)
19792 * @since 1.2.2
19793 */
19794 function _add_fs_theme_activation_dialog() {
19795 $vars = array( 'id' => $this->_module_id );
19796 fs_require_once_template( 'connect.php', $vars );
19797 }
19798
19799 /* Action Links
19800 ------------------------------------------------------------------------------------------------------------------*/
19801 private $_action_links_hooked = false;
19802 private $_action_links = array();
19803
19804 /**
19805 * Hook to plugin action links filter.
19806 *
19807 * @author Vova Feldman (@svovaf)
19808 * @since 1.0.0
19809 */
19810 private function hook_plugin_action_links() {
19811 $this->_logger->entrance();
19812
19813 $this->_action_links_hooked = true;
19814
19815 $this->_logger->log( 'Adding action links hooks.' );
19816
19817 // Add action link to settings page.
19818 add_filter( 'plugin_action_links_' . $this->_plugin_basename, array(
19819 &$this,
19820 '_modify_plugin_action_links_hook'
19821 ), WP_FS__DEFAULT_PRIORITY, 2 );
19822 add_filter( 'network_admin_plugin_action_links_' . $this->_plugin_basename, array(
19823 &$this,
19824 '_modify_plugin_action_links_hook'
19825 ), WP_FS__DEFAULT_PRIORITY, 2 );
19826 }
19827
19828 /**
19829 * Add plugin action link.
19830 *
19831 * @author Vova Feldman (@svovaf)
19832 * @since 1.0.0
19833 *
19834 * @param $label
19835 * @param $url
19836 * @param bool $external
19837 * @param int $priority
19838 * @param bool $key
19839 */
19840 function add_plugin_action_link( $label, $url, $external = false, $priority = WP_FS__DEFAULT_PRIORITY, $key = false ) {
19841 $this->_logger->entrance();
19842
19843 if ( ! isset( $this->_action_links[ $priority ] ) ) {
19844 $this->_action_links[ $priority ] = array();
19845 }
19846
19847 if ( false === $key ) {
19848 $key = preg_replace( "/[^A-Za-z0-9 ]/", '', strtolower( $label ) );
19849 }
19850
19851 $this->_action_links[ $priority ][] = array(
19852 'label' => $label,
19853 'href' => $url,
19854 'key' => $key,
19855 'external' => $external
19856 );
19857 }
19858
19859 /**
19860 * Adds Upgrade and Add-Ons links to the main Plugins page link actions collection.
19861 *
19862 * @author Vova Feldman (@svovaf)
19863 * @since 1.0.0
19864 */
19865 function _add_upgrade_action_link() {
19866 $this->_logger->entrance();
19867
19868 if ( ! $this->is_paying() && $this->has_paid_plan() ) {
19869 $this->add_plugin_action_link(
19870 $this->get_text_inline( 'Upgrade', 'upgrade' ),
19871 $this->get_upgrade_url(),
19872 false,
19873 7,
19874 'upgrade'
19875 );
19876 }
19877
19878 if ( $this->has_addons() ) {
19879 $this->add_plugin_action_link(
19880 $this->get_text_inline( 'Add-Ons', 'add-ons' ),
19881 $this->_get_admin_page_url( 'addons' ),
19882 false,
19883 9,
19884 'addons'
19885 );
19886 }
19887 }
19888
19889 /**
19890 * Adds "Activate License" or "Change License" link to the main Plugins page link actions collection.
19891 *
19892 * @author Leo Fajardo (@leorw)
19893 * @since 1.1.9
19894 */
19895 function _add_license_action_link() {
19896 $this->_logger->entrance();
19897
19898 if ( ! self::is_ajax() ) {
19899 // Inject license activation dialog UI and client side code.
19900 add_action( 'admin_footer', array( &$this, '_add_license_activation_dialog_box' ) );
19901 }
19902
19903 $link_text = $this->is_free_plan() ?
19904 $this->get_text_inline( 'Activate License', 'activate-license' ) :
19905 $this->get_text_inline( 'Change License', 'change-license' );
19906
19907 $this->add_plugin_action_link(
19908 $link_text,
19909 '#',
19910 false,
19911 11,
19912 ( 'activate-license ' . $this->get_unique_affix() )
19913 );
19914 }
19915
19916 /**
19917 * @author Leo Fajardo (@leorw)
19918 * @since 2.0.2
19919 */
19920 function _add_premium_version_upgrade_selection_action() {
19921 $this->_logger->entrance();
19922
19923 if ( ! self::is_ajax() ) {
19924 add_action( 'admin_footer', array( &$this, '_add_premium_version_upgrade_selection_dialog_box' ) );
19925 }
19926 }
19927
19928 /**
19929 * Adds "Opt in" or "Opt out" link to the main "Plugins" page link actions collection.
19930 *
19931 * @author Leo Fajardo (@leorw)
19932 * @since 1.2.1.5
19933 */
19934 function _add_tracking_links() {
19935 if ( ! current_user_can( 'manage_options' ) ) {
19936 return;
19937 }
19938
19939 $this->_logger->entrance();
19940
19941 /**
19942 * If the activation has been delegated to site admins, no tracking-related actions for now.
19943 *
19944 * @author Leo Fajardo (@leorw)
19945 */
19946 if ( $this->_is_network_active && $this->is_network_delegated_connection() ) {
19947 return;
19948 }
19949
19950 if ( fs_request_is_action_secure( $this->get_unique_affix() . '_reconnect' ) ) {
19951 if ( ! $this->is_registered() && $this->is_anonymous() ) {
19952 $this->connect_again();
19953
19954 return;
19955 }
19956 }
19957
19958 if ( ( $this->is_plugin() && ! self::is_plugins_page() ) ||
19959 ( $this->is_theme() && ! self::is_themes_page() )
19960 ) {
19961 // Only show tracking links on the plugins and themes pages.
19962 return;
19963 }
19964
19965 if ( ! $this->is_enable_anonymous() ) {
19966 // Don't allow to opt-out if anonymous mode is disabled.
19967 return;
19968 }
19969
19970 if ( ! $this->is_free_plan() ) {
19971 // Don't allow to opt-out if running in paid plan.
19972 return;
19973 }
19974
19975 if ( $this->add_ajax_action( 'stop_tracking', array( &$this, '_stop_tracking_callback' ) ) ) {
19976 return;
19977 }
19978
19979 if ( $this->add_ajax_action( 'allow_tracking', array( &$this, '_allow_tracking_callback' ) ) ) {
19980 return;
19981 }
19982
19983 $url = '#';
19984
19985 if ( $this->is_registered() ) {
19986 if ( $this->is_tracking_allowed() ) {
19987 $link_text_id = $this->get_text_inline( 'Opt Out', 'opt-out' );
19988 } else {
19989 $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' );
19990 }
19991
19992 add_action( 'admin_footer', array( &$this, '_add_optout_dialog' ) );
19993 } else {
19994 $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' );
19995
19996 $params = ! $this->is_anonymous() ?
19997 array() :
19998 array(
19999 'nonce' => wp_create_nonce( $this->get_unique_affix() . '_reconnect' ),
20000 'fs_action' => ( $this->get_unique_affix() . '_reconnect' ),
20001 );
20002
20003 $url = $this->get_activation_url( $params );
20004 }
20005
20006 if ( $this->is_plugin() && self::is_plugins_page() ) {
20007 $this->add_plugin_action_link(
20008 $link_text_id,
20009 $url,
20010 false,
20011 13,
20012 "opt-in-or-opt-out {$this->_slug}"
20013 );
20014 }
20015 }
20016
20017 /**
20018 * Get the URL of the page that should be loaded right after the plugin activation.
20019 *
20020 * @author Vova Feldman (@svovaf)
20021 * @since 1.1.7.4
20022 *
20023 * @return string
20024 */
20025 function get_after_plugin_activation_redirect_url() {
20026 $url = false;
20027
20028 if ( ! $this->is_addon() || ! $this->has_free_plan() ) {
20029 $first_time_path = $this->_menu->get_first_time_path();
20030
20031 if ( $this->is_activation_mode() ) {
20032 $url = $this->get_activation_url();
20033 } else if ( ! empty( $first_time_path ) ) {
20034 $url = $first_time_path;
20035 } else {
20036 $page = '';
20037 if ( ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
20038 if ( $this->is_network_registered() ) {
20039 $page = 'account';
20040 } else if ( $this->is_network_anonymous() ) {
20041 $this->maybe_set_slug_and_network_menu_exists_flag();
20042 }
20043 }
20044
20045 $url = $this->_get_admin_page_url( $page );
20046 }
20047 } else {
20048 $plugin_fs = false;
20049
20050 if ( $this->is_parent_plugin_installed() ) {
20051 $plugin_fs = self::get_parent_instance();
20052 }
20053
20054 if ( is_object( $plugin_fs ) ) {
20055 if ( ! $plugin_fs->is_registered() ) {
20056 // Forward to parent plugin connect when parent not registered.
20057 $url = $plugin_fs->get_activation_url();
20058 } else {
20059 // Forward to account page.
20060 $url = $plugin_fs->_get_admin_page_url( 'account' );
20061 }
20062 }
20063 }
20064
20065 return $url;
20066 }
20067
20068 /**
20069 * Forward page to activation page.
20070 *
20071 * @author Vova Feldman (@svovaf)
20072 * @since 1.0.3
20073 */
20074 function _redirect_on_activation_hook() {
20075 $url = $this->get_after_plugin_activation_redirect_url();
20076
20077 if ( is_string( $url ) ) {
20078 fs_redirect( $url );
20079 }
20080 }
20081
20082 /**
20083 * Modify plugin's page action links collection.
20084 *
20085 * @author Vova Feldman (@svovaf)
20086 * @since 1.0.0
20087 *
20088 * @param array $links
20089 * @param $file
20090 *
20091 * @return array
20092 */
20093 function _modify_plugin_action_links_hook( $links, $file ) {
20094 $this->_logger->entrance();
20095
20096 $passed_deactivate = false;
20097 $deactivate_link = '';
20098 $before_deactivate = array();
20099 $after_deactivate = array();
20100 foreach ( $links as $key => $link ) {
20101 if ( 'deactivate' === $key ) {
20102 $deactivate_link = $link;
20103 $passed_deactivate = true;
20104 continue;
20105 }
20106
20107 if ( ! $passed_deactivate ) {
20108 $before_deactivate[ $key ] = $link;
20109 } else {
20110 $after_deactivate[ $key ] = $link;
20111 }
20112 }
20113
20114 ksort( $this->_action_links );
20115
20116 foreach ( $this->_action_links as $new_links ) {
20117 foreach ( $new_links as $link ) {
20118 $before_deactivate[ $link['key'] ] = '<a href="' . $link['href'] . '"' . ( $link['external'] ? ' target="_blank"' : '' ) . '>' . $link['label'] . '</a>';
20119 }
20120 }
20121
20122 if ( ! empty( $deactivate_link ) ) {
20123 /**
20124 * This HTML element is used to identify the correct plugin when attaching an event to its Deactivate link.
20125 *
20126 * @since 1.2.1.6 Always show the deactivation feedback form since we added automatic free version deactivation upon premium code activation.
20127 */
20128 $deactivate_link .= '<i class="fs-module-id" data-module-id="' . $this->_module_id . '"></i>';
20129
20130 // Append deactivation link.
20131 $before_deactivate['deactivate'] = $deactivate_link;
20132 }
20133
20134 return array_merge( $before_deactivate, $after_deactivate );
20135 }
20136
20137 /**
20138 * Adds admin message.
20139 *
20140 * @author Vova Feldman (@svovaf)
20141 * @since 1.0.4
20142 *
20143 * @param string $message
20144 * @param string $title
20145 * @param string $type
20146 */
20147 function add_admin_message( $message, $title = '', $type = 'success' ) {
20148 $this->_admin_notices->add( $message, $title, $type );
20149 }
20150
20151 /**
20152 * Adds sticky admin message.
20153 *
20154 * @author Vova Feldman (@svovaf)
20155 * @since 1.1.0
20156 *
20157 * @param string $message
20158 * @param string $id
20159 * @param string $title
20160 * @param string $type
20161 */
20162 function add_sticky_admin_message( $message, $id, $title = '', $type = 'success' ) {
20163 $this->_admin_notices->add_sticky( $message, $id, $title, $type );
20164 }
20165
20166 /**
20167 * Check if the paid version of the module is installed.
20168 *
20169 * @author Vova Feldman (@svovaf)
20170 * @since 2.2.0
20171 *
20172 * @return bool
20173 */
20174 private function is_premium_version_installed() {
20175 $premium_plugin_basename = $this->premium_plugin_basename();
20176 $premium_plugin = get_plugins( '/' . dirname( $premium_plugin_basename ) );
20177
20178 return ! empty( $premium_plugin );
20179 }
20180
20181 /**
20182 * Helper function that returns the final steps for the upgrade completion.
20183 *
20184 * If the module is already running the premium code, returns an empty string.
20185 *
20186 * @author Vova Feldman (@svovaf)
20187 * @since 1.2.1
20188 *
20189 * @param string $plan_title
20190 *
20191 * @return string
20192 */
20193 private function get_complete_upgrade_instructions( $plan_title = '' ) {
20194 $this->_logger->entrance();
20195
20196 $activate_license_string = $this->get_license_network_activation_notice();
20197
20198 if ( ! $this->has_premium_version() || $this->is_premium() ) {
20199 return '' . $activate_license_string;
20200 }
20201
20202 if ( empty( $plan_title ) ) {
20203 $plan_title = $this->get_plan_title();
20204 }
20205
20206 if ( $this->is_premium_version_installed() ) {
20207 /**
20208 * If the premium version is already installed, instead of showing the installation instructions,
20209 * tell the current user to activate it.
20210 *
20211 * @author Leo Fajardo (@leorw)
20212 * @since 2.2.1
20213 */
20214 $premium_plugin_basename = $this->premium_plugin_basename();
20215
20216 return sprintf(
20217 /* translators: %1s: Product title; %2s: Plan title */
20218 $this->get_text_inline( ' The paid version of %1s is already installed. Please activate it to start benefiting the %2s features. %3s', 'activate-premium-version' ),
20219 sprintf( '<em>%s</em>', esc_html( $this->get_plugin_title() ) ),
20220 $plan_title,
20221 sprintf(
20222 '<a style="margin-left: 10px;" href="%s"><button class="button button-primary">%s</button></a>',
20223 wp_nonce_url( 'plugins.php?action=activate&amp;plugin=' . $premium_plugin_basename, 'activate-plugin_' . $premium_plugin_basename ),
20224 esc_html( sprintf(
20225 /* translators: %s: Plan title */
20226 $this->get_text_inline( 'Activate %s features', 'activate-x-features' ),
20227 $plan_title
20228 ) )
20229 )
20230 );
20231 } else {
20232 // @since 1.2.1.5 The free version is auto deactivated.
20233 $deactivation_step = version_compare( $this->version, '1.2.1.5', '<' ) ?
20234 ( '<li>' . $this->esc_html_inline( 'Deactivate the free version', 'deactivate-free-version' ) . '.</li>' ) :
20235 '';
20236
20237 return sprintf(
20238 ' %s: <ol><li>%s.</li>%s<li>%s (<a href="%s" target="_blank">%s</a>).</li></ol>',
20239 $this->get_text_inline( 'Please follow these steps to complete the upgrade', 'follow-steps-to-complete-upgrade' ),
20240 ( empty( $activate_license_string ) ? '' : $activate_license_string . '</li><li>' ) .
20241 $this->get_latest_download_link( sprintf(
20242 /* translators: %s: Plan title */
20243 $this->get_text_inline( 'Download the latest %s version', 'download-latest-x-version' ),
20244 $plan_title
20245 ) ),
20246 $deactivation_step,
20247 $this->get_text_inline( 'Upload and activate the downloaded version', 'upload-and-activate' ),
20248 '//bit.ly/upload-wp-' . $this->_module_type . 's',
20249 $this->get_text_inline( 'How to upload and activate?', 'howto-upload-activate' )
20250 );
20251 }
20252 }
20253
20254 /**
20255 * @author Leo Fajardo (@leorw)
20256 * @since 2.1.0
20257 *
20258 * @param string $url
20259 * @param array $request
20260 */
20261 private static function enrich_request_for_debug( &$url, &$request ) {
20262 if ( WP_FS__DEBUG_SDK || isset( $_COOKIE['XDEBUG_SESSION'] ) ) {
20263 $url = add_query_arg( 'XDEBUG_SESSION_START', rand( 0, 9999999 ), $url );
20264 $url = add_query_arg( 'XDEBUG_SESSION', 'PHPSTORM', $url );
20265
20266 $request['cookies'] = array(
20267 new WP_Http_Cookie( array(
20268 'name' => 'XDEBUG_SESSION',
20269 'value' => 'PHPSTORM',
20270 ) )
20271 );
20272 }
20273 }
20274
20275 /**
20276 * @author Leo Fajardo (@leorw)
20277 * @since 2.1.0
20278 *
20279 * @param string $url
20280 * @param array $request
20281 * @param int $success_cache_expiration
20282 * @param int $failure_cache_expiration
20283 * @param bool $maybe_enrich_request_for_debug
20284 *
20285 * @return WP_Error|array
20286 */
20287 static function safe_remote_post(
20288 &$url,
20289 $request,
20290 $success_cache_expiration = 0,
20291 $failure_cache_expiration = 0,
20292 $maybe_enrich_request_for_debug = true
20293 ) {
20294 $should_cache = ($success_cache_expiration + $failure_cache_expiration > 0);
20295
20296 $cache_key = $should_cache ? md5( fs_strip_url_protocol($url) . json_encode( $request ) ) : false;
20297
20298 $response = (!WP_FS__DEBUG_SDK && ( false !== $cache_key )) ?
20299 get_transient( $cache_key ) :
20300 false;
20301
20302 if ( false === $response ) {
20303 if ( $maybe_enrich_request_for_debug ) {
20304 self::enrich_request_for_debug( $url, $request );
20305 }
20306
20307 $response = wp_remote_post( $url, $request );
20308
20309 if ( $response instanceof WP_Error ) {
20310 if ( 'https://' === substr( $url, 0, 8 ) &&
20311 isset( $response->errors ) &&
20312 isset( $response->errors['http_request_failed'] )
20313 ) {
20314 $http_error = strtolower( $response->errors['http_request_failed'][0] );
20315
20316 if ( false !== strpos( $http_error, 'ssl' ) ||
20317 false !== strpos( $http_error, 'curl error 35' )
20318 ) {
20319 // Failed due to old version of cURL or Open SSL (SSLv3 is not supported by CloudFlare).
20320 $url = 'http://' . substr( $url, 8 );
20321
20322 $request['timeout'] = 15;
20323 $response = wp_remote_post( $url, $request );
20324 }
20325 }
20326 }
20327
20328 if ( false !== $cache_key ) {
20329 set_transient(
20330 $cache_key,
20331 $response,
20332 ( ( $response instanceof WP_Error ) ?
20333 $failure_cache_expiration :
20334 $success_cache_expiration )
20335 );
20336 }
20337 }
20338
20339 return $response;
20340 }
20341
20342 /**
20343 * This method is used to enrich the after upgrade notice instructions when the upgraded
20344 * license cannot be activated network wide (license quota isn't large enough).
20345 *
20346 * @author Vova Feldman (@svovaf)
20347 * @since 2.0.0
20348 *
20349 * @return string
20350 */
20351 private function get_license_network_activation_notice() {
20352 if ( ! $this->_is_network_active ) {
20353 // Module isn't network level activated.
20354 return '';
20355 }
20356
20357 if ( ! fs_is_network_admin() ) {
20358 // Not network level admin.
20359 return '';
20360 }
20361
20362 if ( get_blog_count() == 1 ) {
20363 // There's only a single site in the network so if there's a context license it was already activated.
20364 return '';
20365 }
20366
20367 if ( ! is_object( $this->_license ) ) {
20368 // No context license.
20369 return '';
20370 }
20371
20372 if ( $this->_license->is_single_site() && 0 < $this->_license->activated ) {
20373 // 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).
20374 return '';
20375 }
20376
20377 if ( $this->can_activate_license_on_network( $this->_license ) ) {
20378 // 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).
20379 return '';
20380 }
20381
20382 return sprintf(
20383 $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' ),
20384 '<a href="' . $this->get_account_url( false, array( 'activate_license' => 'true' ) ) . '">',
20385 '</a>'
20386 );
20387 }
20388
20389 /**
20390 * @author Vova Feldman (@svovaf)
20391 * @since 1.2.1.7
20392 *
20393 * @param string $key
20394 *
20395 * @return string
20396 */
20397 function get_text( $key ) {
20398 return fs_text( $key, $this->_slug );
20399 }
20400
20401 /**
20402 * @author Vova Feldman (@svovaf)
20403 * @since 1.2.3
20404 *
20405 * @param string $text Translatable string.
20406 * @param string $key String key for overrides.
20407 *
20408 * @return string
20409 */
20410 function get_text_inline( $text, $key = '' ) {
20411 return _fs_text_inline( $text, $key, $this->_slug );
20412 }
20413
20414 /**
20415 * @author Vova Feldman (@svovaf)
20416 * @since 1.2.3
20417 *
20418 * @param string $text Translatable string.
20419 * @param string $context Context information for the translators.
20420 * @param string $key String key for overrides.
20421 *
20422 * @return string
20423 */
20424 function get_text_x_inline( $text, $context, $key ) {
20425 return _fs_text_x_inline( $text, $context, $key, $this->_slug );
20426 }
20427
20428 /**
20429 * @author Vova Feldman (@svovaf)
20430 * @since 1.2.3
20431 *
20432 * @param string $text Translatable string.
20433 * @param string $key String key for overrides.
20434 *
20435 * @return string
20436 */
20437 function esc_html_inline( $text, $key ) {
20438 return esc_html( _fs_text_inline( $text, $key, $this->_slug ) );
20439 }
20440
20441 #----------------------------------------------------------------------------------
20442 #region Versioning
20443 #----------------------------------------------------------------------------------
20444
20445 /**
20446 * Check if Freemius in SDK upgrade mode.
20447 *
20448 * @author Vova Feldman (@svovaf)
20449 * @since 1.0.9
20450 *
20451 * @return bool
20452 */
20453 function is_sdk_upgrade_mode() {
20454 return isset( $this->_storage->sdk_upgrade_mode ) ?
20455 $this->_storage->sdk_upgrade_mode :
20456 false;
20457 }
20458
20459 /**
20460 * Turn SDK upgrade mode off.
20461 *
20462 * @author Vova Feldman (@svovaf)
20463 * @since 1.0.9
20464 */
20465 function set_sdk_upgrade_complete() {
20466 $this->_storage->sdk_upgrade_mode = false;
20467 }
20468
20469 /**
20470 * Check if plugin upgrade mode.
20471 *
20472 * @author Vova Feldman (@svovaf)
20473 * @since 1.0.9
20474 *
20475 * @return bool
20476 */
20477 function is_plugin_upgrade_mode() {
20478 return isset( $this->_storage->plugin_upgrade_mode ) ?
20479 $this->_storage->plugin_upgrade_mode :
20480 false;
20481 }
20482
20483 /**
20484 * Turn plugin upgrade mode off.
20485 *
20486 * @author Vova Feldman (@svovaf)
20487 * @since 1.0.9
20488 */
20489 function set_plugin_upgrade_complete() {
20490 $this->_storage->plugin_upgrade_mode = false;
20491 }
20492
20493 #endregion
20494
20495 #----------------------------------------------------------------------------------
20496 #region Permissions
20497 #----------------------------------------------------------------------------------
20498
20499 /**
20500 * Check if specific permission requested.
20501 *
20502 * @author Vova Feldman (@svovaf)
20503 * @since 1.1.6
20504 *
20505 * @param string $permission
20506 *
20507 * @return bool
20508 */
20509 function is_permission_requested( $permission ) {
20510 return isset( $this->_permissions[ $permission ] ) && ( true === $this->_permissions[ $permission ] );
20511 }
20512
20513 #endregion
20514
20515 #----------------------------------------------------------------------------------
20516 #region Auto Activation
20517 #----------------------------------------------------------------------------------
20518
20519 /**
20520 * Hints the SDK if running an auto-installation.
20521 *
20522 * @var bool
20523 */
20524 private $_isAutoInstall = false;
20525
20526 /**
20527 * After upgrade callback to install and auto activate a plugin.
20528 * This code will only be executed on explicit request from the user,
20529 * following the practice Jetpack are using with their theme installations.
20530 *
20531 * @link https://make.wordpress.org/plugins/2017/03/16/clarification-of-guideline-8-executable-code-and-installs/
20532 *
20533 * @author Vova Feldman (@svovaf)
20534 * @since 1.2.1.7
20535 */
20536 function _install_premium_version_ajax_action() {
20537 $this->_logger->entrance();
20538
20539 $this->check_ajax_referer( 'install_premium_version' );
20540
20541 if ( ! $this->is_registered() ) {
20542 // Not registered.
20543 self::shoot_ajax_failure( array(
20544 'message' => $this->get_text_inline( 'Auto installation only works for opted-in users.', 'auto-install-error-not-opted-in' ),
20545 'code' => 'premium_installed',
20546 ) );
20547 }
20548
20549 $plugin_id = fs_request_get( 'target_module_id', $this->get_id() );
20550
20551 if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
20552 // Invalid ID.
20553 self::shoot_ajax_failure( array(
20554 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
20555 'code' => 'invalid_module_id',
20556 ) );
20557 }
20558
20559 if ( $plugin_id == $this->get_id() ) {
20560 if ( $this->is_premium() ) {
20561 // Already using the premium code version.
20562 self::shoot_ajax_failure( array(
20563 'message' => $this->get_text_inline( 'Premium version already active.', 'auto-install-error-premium-activated' ),
20564 'code' => 'premium_installed',
20565 ) );
20566 }
20567 if ( ! $this->can_use_premium_code() ) {
20568 // Don't have access to the premium code.
20569 self::shoot_ajax_failure( array(
20570 'message' => $this->get_text_inline( 'You do not have a valid license to access the premium version.', 'auto-install-error-invalid-license' ),
20571 'code' => 'invalid_license',
20572 ) );
20573 }
20574 if ( ! $this->has_release_on_freemius() ) {
20575 // Plugin is a serviceware, no premium code version.
20576 self::shoot_ajax_failure( array(
20577 'message' => $this->get_text_inline( 'Plugin is a "Serviceware" which means it does not have a premium code version.', 'auto-install-error-serviceware' ),
20578 'code' => 'premium_version_missing',
20579 ) );
20580 }
20581 } else {
20582 $addon = $this->get_addon( $plugin_id );
20583
20584 if ( ! is_object( $addon ) ) {
20585 // Invalid add-on ID.
20586 self::shoot_ajax_failure( array(
20587 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
20588 'code' => 'invalid_module_id',
20589 ) );
20590 }
20591
20592 if ( $this->is_addon_activated( $plugin_id, true ) ) {
20593 // Premium add-on version is already activated.
20594 self::shoot_ajax_failure( array(
20595 'message' => $this->get_text_inline( 'Premium add-on version already installed.', 'auto-install-error-premium-addon-activated' ),
20596 'code' => 'premium_installed',
20597 ) );
20598 }
20599 }
20600
20601 $this->_isAutoInstall = true;
20602
20603 // Try to install and activate.
20604 $updater = FS_Plugin_Updater::instance( $this );
20605 $result = $updater->install_and_activate_plugin( $plugin_id );
20606
20607 if ( is_array( $result ) && ! empty( $result['message'] ) ) {
20608 self::shoot_ajax_failure( array(
20609 'message' => $result['message'],
20610 'code' => $result['code'],
20611 ) );
20612 }
20613
20614 self::shoot_ajax_success( $result );
20615 }
20616
20617 /**
20618 * Displays module activation dialog box after a successful upgrade
20619 * where the user explicitly requested to auto download and install
20620 * the premium version.
20621 *
20622 * @author Vova Feldman (@svovaf)
20623 * @since 1.2.1.7
20624 */
20625 function _add_auto_installation_dialog_box() {
20626 $this->_logger->entrance();
20627
20628 if ( ! $this->is_registered() ) {
20629 // Not registered.
20630 return;
20631 }
20632
20633 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
20634
20635 if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
20636 // Invalid module ID.
20637 return;
20638 }
20639
20640 if ( $plugin_id == $this->get_id() ) {
20641 if ( $this->is_premium() ) {
20642 // Already using the premium code version.
20643 return;
20644 }
20645 if ( ! $this->can_use_premium_code() ) {
20646 // Don't have access to the premium code.
20647 return;
20648 }
20649 if ( ! $this->has_release_on_freemius() ) {
20650 // Plugin is a serviceware, no premium code version.
20651 return;
20652 }
20653 } else {
20654 $addon = $this->get_addon( $plugin_id );
20655
20656 if ( ! is_object( $addon ) ) {
20657 // Invalid add-on ID.
20658 return;
20659 }
20660
20661 if ( $this->is_addon_activated( $plugin_id, true ) ) {
20662 // Premium add-on version is already activated.
20663 return;
20664 }
20665 }
20666
20667 $vars = array(
20668 'id' => $this->_module_id,
20669 'target_module_id' => $plugin_id,
20670 'slug' => $this->_slug,
20671 );
20672
20673 fs_require_template( 'auto-installation.php', $vars );
20674 }
20675
20676 #endregion
20677
20678 #--------------------------------------------------------------------------------
20679 #region Tabs Integration
20680 #--------------------------------------------------------------------------------
20681
20682 #region Module's Original Tabs
20683
20684 /**
20685 * Inject a JavaScript logic to capture the theme tabs HTML.
20686 *
20687 * @author Vova Feldman (@svovaf)
20688 * @since 1.2.2.7
20689 */
20690 function _tabs_capture() {
20691 $this->_logger->entrance();
20692
20693 if ( ! $this->is_theme_settings_page() ||
20694 ! $this->is_matching_url( $this->main_menu_url() )
20695 ) {
20696 return;
20697 }
20698
20699 $params = array(
20700 'id' => $this->_module_id,
20701 );
20702
20703 fs_require_once_template( 'tabs-capture-js.php', $params );
20704 }
20705
20706 /**
20707 * Cache theme's tabs HTML for a week. The cache will also be set as expired
20708 * after version and type (free/premium) changes, in addition to the week period.
20709 *
20710 * @author Vova Feldman (@svovaf)
20711 * @since 1.2.2.7
20712 */
20713 function _store_tabs_ajax_action() {
20714 $this->_logger->entrance();
20715
20716 $this->check_ajax_referer( 'store_tabs' );
20717
20718 // Init filesystem if not yet initiated.
20719 WP_Filesystem();
20720
20721 // Get POST body HTML data.
20722 global $wp_filesystem;
20723 $tabs_html = $wp_filesystem->get_contents( "php://input" );
20724
20725 if ( is_string( $tabs_html ) ) {
20726 $tabs_html = trim( $tabs_html );
20727 }
20728
20729 if ( ! is_string( $tabs_html ) || empty( $tabs_html ) ) {
20730 self::shoot_ajax_failure();
20731 }
20732
20733 $this->_cache->set( 'tabs', $tabs_html, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
20734
20735 self::shoot_ajax_success();
20736 }
20737
20738 /**
20739 * Cache theme's settings page custom styles. The cache will also be set as expired
20740 * after version and type (free/premium) changes, in addition to the week period.
20741 *
20742 * @author Vova Feldman (@svovaf)
20743 * @since 1.2.2.7
20744 */
20745 function _store_tabs_styles() {
20746 $this->_logger->entrance();
20747
20748 if ( ! $this->is_theme_settings_page() ||
20749 ! $this->is_matching_url( $this->main_menu_url() )
20750 ) {
20751 return;
20752 }
20753
20754 $wp_styles = wp_styles();
20755
20756 $theme_styles_url = get_template_directory_uri();
20757
20758 $stylesheets = array();
20759 foreach ( $wp_styles->queue as $handler ) {
20760 if ( fs_starts_with( $handler, 'fs_' ) ) {
20761 // Assume that stylesheets that their handler starts with "fs_" belong to the SDK.
20762 continue;
20763 }
20764
20765 /**
20766 * @var _WP_Dependency $stylesheet
20767 */
20768 $stylesheet = $wp_styles->registered[ $handler ];
20769
20770 if ( fs_starts_with( $stylesheet->src, $theme_styles_url ) ) {
20771 $stylesheets[] = $stylesheet->src;
20772 }
20773 }
20774
20775 if ( ! empty( $stylesheets ) ) {
20776 $this->_cache->set( 'tabs_stylesheets', $stylesheets, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
20777 }
20778 }
20779
20780 /**
20781 * Check if module's original settings page has any tabs.
20782 *
20783 * @author Vova Feldman (@svovaf)
20784 * @since 1.2.2.7
20785 *
20786 * @return bool
20787 */
20788 private function has_tabs() {
20789 return $this->_cache->has( 'tabs' );
20790 }
20791
20792 /**
20793 * Get module's settings page HTML content, starting
20794 * from the beginning of the <div class="wrap"> element,
20795 * until the tabs HTML (including).
20796 *
20797 * @author Vova Feldman (@svovaf)
20798 * @since 1.2.2.7
20799 *
20800 * @return string
20801 */
20802 private function get_tabs_html() {
20803 $this->_logger->entrance();
20804
20805 return $this->_cache->get( 'tabs' );
20806 }
20807
20808 /**
20809 * Check if page should include tabs.
20810 *
20811 * @author Vova Feldman (@svovaf)
20812 * @since 1.2.2.7
20813 *
20814 * @return bool
20815 */
20816 private function should_page_include_tabs() {
20817 if ( ! $this->has_settings_menu() ) {
20818 // Don't add tabs if no settings at all.
20819 return false;
20820 }
20821
20822 if ( ! $this->is_theme() ) {
20823 // Only add tabs to themes for now.
20824 return false;
20825 }
20826
20827 if ( ! $this->has_paid_plan() && ! $this->has_addons() ) {
20828 // Only add tabs to monetizing themes.
20829 return false;
20830 }
20831
20832 if ( ! $this->is_theme_settings_page() ) {
20833 // Only add tabs if browsing one of the theme's setting pages.
20834 return false;
20835 }
20836
20837 if ( $this->is_admin_page( 'pricing' ) && fs_request_get_bool( 'checkout' ) ) {
20838 // Don't add tabs on checkout page, we want to reduce distractions
20839 // as much as possible.
20840 return false;
20841 }
20842
20843 return true;
20844 }
20845
20846 /**
20847 * Add the tabs HTML before the setting's page content and
20848 * enqueue any required stylesheets.
20849 *
20850 * @author Vova Feldman (@svovaf)
20851 * @since 1.2.2.7
20852 *
20853 * @return bool If tabs were included.
20854 */
20855 function _add_tabs_before_content() {
20856 $this->_logger->entrance();
20857
20858 if ( ! $this->should_page_include_tabs() ) {
20859 return false;
20860 }
20861
20862 /**
20863 * Enqueue the original stylesheets that are included in the
20864 * theme settings page. That way, if the theme settings has
20865 * some custom _styled_ content above the tabs UI, this
20866 * will make sure that the styling is preserved.
20867 */
20868 $stylesheets = $this->_cache->get( 'tabs_stylesheets', array() );
20869 if ( is_array( $stylesheets ) ) {
20870 for ( $i = 0, $len = count( $stylesheets ); $i < $len; $i ++ ) {
20871 wp_enqueue_style( "fs_{$this->_module_id}_tabs_{$i}", $stylesheets[ $i ] );
20872 }
20873 }
20874
20875 // Cut closing </div> tag.
20876 echo substr( trim( $this->get_tabs_html() ), 0, - 6 );
20877
20878 return true;
20879 }
20880
20881 /**
20882 * Add the tabs closing HTML after the setting's page content.
20883 *
20884 * @author Vova Feldman (@svovaf)
20885 * @since 1.2.2.7
20886 *
20887 * @return bool If tabs closing HTML was included.
20888 */
20889 function _add_tabs_after_content() {
20890 $this->_logger->entrance();
20891
20892 if ( ! $this->should_page_include_tabs() ) {
20893 return false;
20894 }
20895
20896 echo '</div>';
20897
20898 return true;
20899 }
20900
20901 #endregion
20902
20903 /**
20904 * Add in-page JavaScript to inject the Freemius tabs into
20905 * the module's setting tabs section.
20906 *
20907 * @author Vova Feldman (@svovaf)
20908 * @since 1.2.2.7
20909 */
20910 function _add_freemius_tabs() {
20911 $this->_logger->entrance();
20912
20913 if ( ! $this->should_page_include_tabs() ) {
20914 return;
20915 }
20916
20917 $params = array( 'id' => $this->_module_id );
20918 fs_require_once_template( 'tabs.php', $params );
20919 }
20920
20921 #endregion
20922
20923 #--------------------------------------------------------------------------------
20924 #region Customizer Integration for Themes
20925 #--------------------------------------------------------------------------------
20926
20927 /**
20928 * @author Vova Feldman (@svovaf)
20929 * @since 1.2.2.7
20930 *
20931 * @param WP_Customize_Manager $customizer
20932 */
20933 function _customizer_register( $customizer ) {
20934 $this->_logger->entrance();
20935
20936 if ( $this->is_pricing_page_visible() ) {
20937 require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-upsell-control.php';
20938
20939 $customizer->add_section( 'freemius_upsell', array(
20940 'title' => '&#9733; ' . $this->get_text_inline( 'View paid features', 'view-paid-features' ),
20941 'priority' => 1,
20942 ) );
20943 $customizer->add_setting( 'freemius_upsell', array(
20944 'sanitize_callback' => 'esc_html',
20945 ) );
20946
20947 $customizer->add_control( new FS_Customizer_Upsell_Control( $customizer, 'freemius_upsell', array(
20948 'fs' => $this,
20949 'section' => 'freemius_upsell',
20950 'priority' => 100,
20951 ) ) );
20952 }
20953
20954 if ( $this->is_page_visible( 'contact' ) || $this->is_page_visible( 'support' ) ) {
20955 require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-support-section.php';
20956
20957 // Main Documentation Link In Customizer Root.
20958 $customizer->add_section( new FS_Customizer_Support_Section( $customizer, 'freemius_support', array(
20959 'fs' => $this,
20960 'priority' => 1000,
20961 ) ) );
20962 }
20963 }
20964
20965 #endregion
20966
20967 /**
20968 * If the theme has a paid version, add some custom
20969 * styling to the theme's premium version (if exists)
20970 * to highlight that it's the premium version of the
20971 * same theme, making it easier for identification
20972 * after the user upgrades and upload it to the site.
20973 *
20974 * @author Vova Feldman (@svovaf)
20975 * @since 1.2.2.7
20976 */
20977 function _style_premium_theme() {
20978 $this->_logger->entrance();
20979
20980 if ( ! self::is_themes_page() ) {
20981 // Only include in the themes page.
20982 return;
20983 }
20984
20985 if ( ! $this->has_paid_plan() ) {
20986 // Only include if has any paid plans.
20987 return;
20988 }
20989
20990 $params = null;
20991 fs_require_once_template( '/js/jquery.content-change.php', $params );
20992
20993 $params = array(
20994 'slug' => $this->_slug,
20995 'id' => $this->_module_id,
20996 );
20997
20998 fs_require_template( '/js/style-premium-theme.php', $params );
20999 }
21000
21001 /**
21002 * This method will return the absolute URL of the module's local icon.
21003 *
21004 * When you are running your plugin or theme on a **localhost** environment, if the icon
21005 * is not found in the local assets folder, try to fetch the icon URL from Freemius. If not set and
21006 * it's a plugin hosted on WordPress.org, try fetching the icon URL from wordpress.org.
21007 * If an icon is found, this method will automatically attempt to download the icon and store it
21008 * in /freemius/assets/img/{slug}.{png|jpg|gif|svg}.
21009 *
21010 * It's important to mention that this method is NOT phoning home since the developer will deploy
21011 * the product with the local icon in the assets folder. The download process just simplifies
21012 * the process for the developer.
21013 *
21014 * @author Vova Feldman (@svovaf)
21015 * @since 2.0.0
21016 *
21017 * @return string
21018 */
21019 function get_local_icon_url() {
21020 global $fs_active_plugins;
21021
21022 /**
21023 * @since 1.1.7.5
21024 */
21025 $local_path = $this->apply_filters( 'plugin_icon', false );
21026
21027 if ( is_string( $local_path ) ) {
21028 $icons = array( $local_path );
21029 } else {
21030 $img_dir = WP_FS__DIR_IMG;
21031
21032 // Locate the main assets folder.
21033 if ( 1 < count( $fs_active_plugins->plugins ) ) {
21034 $plugin_or_theme_img_dir = ( $this->is_plugin() ? WP_PLUGIN_DIR : get_theme_root( get_stylesheet() ) );
21035
21036 foreach ( $fs_active_plugins->plugins as $sdk_path => &$data ) {
21037 if ( $data->plugin_path == $this->get_plugin_basename() ) {
21038 $img_dir = $plugin_or_theme_img_dir
21039 . '/'
21040 /**
21041 * The basename will be `themes` or the basename of a custom themes directory.
21042 *
21043 * @author Leo Fajardo (@leorw)
21044 * @since 2.2.3
21045 */
21046 . str_replace( '../' . basename( $plugin_or_theme_img_dir ) . '/', '', $sdk_path )
21047 . '/assets/img';
21048
21049 break;
21050 }
21051 }
21052 }
21053
21054 // Try to locate the icon in the assets folder.
21055 $icons = glob( fs_normalize_path( $img_dir . "/{$this->_slug}.*" ) );
21056
21057 if ( ! is_array( $icons ) || 0 === count( $icons ) ) {
21058 if ( ! WP_FS__IS_LOCALHOST && $this->is_theme() ) {
21059 $icons = array(
21060 fs_normalize_path( $img_dir . '/theme-icon.png' )
21061 );
21062 } else {
21063 $icon_found = false;
21064 $local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.png" );
21065
21066 if ( ! function_exists( 'get_filesystem_method' ) ) {
21067 require_once ABSPATH . 'wp-admin/includes/file.php';
21068 }
21069
21070 $have_write_permissions = ( 'direct' === get_filesystem_method( array(), fs_normalize_path( $img_dir ) ) );
21071
21072 /**
21073 * IMPORTANT: THIS CODE WILL NEVER RUN AFTER THE PLUGIN IS IN THE REPO.
21074 *
21075 * This code will only be executed once during the testing
21076 * of the plugin in a local environment. The plugin icon file WILL
21077 * already exist in the assets folder when the plugin is deployed to
21078 * the repository.
21079 */
21080 if ( WP_FS__IS_LOCALHOST && $have_write_permissions ) {
21081 // Fetch icon from Freemius.
21082 $icon = $this->fetch_remote_icon_url();
21083
21084 // Fetch icon from WordPress.org.
21085 if ( empty( $icon ) && $this->is_plugin() && $this->is_org_repo_compliant() ) {
21086 if ( ! function_exists( 'plugins_api' ) ) {
21087 require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
21088 }
21089
21090 $plugin_information = plugins_api( 'plugin_information', array(
21091 'slug' => $this->_slug,
21092 'fields' => array(
21093 'sections' => false,
21094 'tags' => false,
21095 'icons' => true
21096 )
21097 ) );
21098
21099 if (
21100 ! is_wp_error( $plugin_information )
21101 && isset( $plugin_information->icons )
21102 && ! empty( $plugin_information->icons )
21103 ) {
21104 /**
21105 * Get the smallest icon.
21106 *
21107 * @author Leo Fajardo (@leorw)
21108 * @since 1.2.2
21109 */
21110 $icon = end( $plugin_information->icons );
21111 }
21112 }
21113
21114 if ( ! empty( $icon ) ) {
21115 if ( 0 !== strpos( $icon, 'http' ) ) {
21116 $icon = 'http:' . $icon;
21117 }
21118
21119 /**
21120 * Get a clean file extension, e.g.: "jpg" and not "jpg?rev=1305765".
21121 *
21122 * @author Leo Fajardo (@leorw)
21123 * @since 1.2.2
21124 */
21125 $ext = pathinfo( strtok( $icon, '?' ), PATHINFO_EXTENSION );
21126
21127 $local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.{$ext}" );
21128
21129 // Try to download the icon.
21130 $icon_found = fs_download_image( $icon, $local_path );
21131 }
21132 }
21133
21134 if ( ! $icon_found ) {
21135 // No icons found, fallback to default icon.
21136 if ( $have_write_permissions ) {
21137 // If have write permissions, copy default icon.
21138 copy( fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" ), $local_path );
21139 } else {
21140 // If doesn't have write permissions, use default icon path.
21141 $local_path = fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" );
21142 }
21143 }
21144
21145 $icons = array( $local_path );
21146 }
21147 }
21148 }
21149
21150 $icon_dir = dirname( $icons[0] );
21151
21152 return fs_img_url( substr( $icons[0], strlen( $icon_dir ) ), $icon_dir );
21153 }
21154
21155 /**
21156 * Fetch module's extended info.
21157 *
21158 * @author Vova Feldman (@svovaf)
21159 * @since 2.0.0
21160 *
21161 * @return object|mixed
21162 */
21163 private function fetch_module_info() {
21164 return $this->get_api_plugin_scope()->get( 'info.json', false, WP_FS__TIME_WEEK_IN_SEC );
21165 }
21166
21167 /**
21168 * Fetch module's remote icon URL.
21169 *
21170 * @author Vova Feldman (@svovaf)
21171 * @since 2.0.0
21172 *
21173 * @return string
21174 */
21175 function fetch_remote_icon_url() {
21176 $info = $this->fetch_module_info();
21177
21178 return ( $this->is_api_result_object( $info, 'icon' ) && is_string( $info->icon ) ) ?
21179 $info->icon :
21180 '';
21181 }
21182
21183 #--------------------------------------------------------------------------------
21184 #region GDPR
21185 #--------------------------------------------------------------------------------
21186
21187 /**
21188 * @author Leo Fajardo (@leorw)
21189 * @since 2.1.0
21190 *
21191 * @return bool
21192 */
21193 function fetch_and_store_current_user_gdpr_anonymously() {
21194 $pong = $this->ping( null, true );
21195
21196 if ( ! $this->get_api_plugin_scope()->is_valid_ping( $pong ) ) {
21197 return false;
21198 } else {
21199 FS_GDPR_Manager::instance()->store_is_required( $pong->is_gdpr_required );
21200
21201 return $pong->is_gdpr_required;
21202 }
21203 }
21204
21205 /**
21206 * @author Leo Fajardo (@leorw)
21207 * @since 2.1.0
21208 *
21209 * @param array $user_plugins
21210 *
21211 * @return string
21212 */
21213 private function get_gdpr_admin_notice_string( $user_plugins ) {
21214 $this->_logger->entrance();
21215
21216 $addons = self::get_all_addons();
21217
21218 foreach ( $user_plugins as $user_plugin ) {
21219 $has_addons = isset( $addons[ $user_plugin->id ] );
21220
21221 if ( WP_FS__MODULE_TYPE_PLUGIN === $user_plugin->type && ! $has_addons ) {
21222 if ( $this->_module_id == $user_plugin->id ) {
21223 $addons = $this->get_addons();
21224 $has_addons = ( ! empty( $addons ) );
21225 } else {
21226 $plugin_api = FS_Api::instance(
21227 $user_plugin->id,
21228 'plugin',
21229 $user_plugin->id,
21230 $user_plugin->public_key,
21231 ! $user_plugin->is_live
21232 );
21233
21234 $addons_result = $plugin_api->get( '/addons.json?enriched=true', true );
21235
21236 if ( $this->is_api_result_object( $addons_result, 'plugins' ) &&
21237 is_array( $addons_result->plugins ) &&
21238 ! empty( $addons_result->plugins )
21239 ) {
21240 $has_addons = true;
21241 }
21242 }
21243 }
21244
21245 $user_plugin->has_addons = $has_addons;
21246 }
21247
21248 $is_single_parent_product = ( 1 === count( $user_plugins ) );
21249
21250 $multiple_products_text = '';
21251
21252 if ( $is_single_parent_product ) {
21253 $single_parent_product = reset( $user_plugins );
21254
21255 $thank_you = sprintf(
21256 "<span data-plugin-id='%d'>%s</span>",
21257 $single_parent_product->id,
21258 sprintf(
21259 $single_parent_product->has_addons ?
21260 $this->get_text_inline( 'Thank you so much for using %s and its add-ons!', 'thank-you-for-using-product-and-its-addons' ) :
21261 $this->get_text_inline( 'Thank you so much for using %s!', 'thank-you-for-using-product' ),
21262 sprintf('<b><i>%s</i></b>', $single_parent_product->title)
21263 )
21264 );
21265
21266 $already_opted_in = sprintf(
21267 $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' ),
21268 ( WP_FS__MODULE_TYPE_THEME === $single_parent_product->type ) ? WP_FS__MODULE_TYPE_THEME : WP_FS__MODULE_TYPE_PLUGIN
21269 );
21270 } else {
21271 $thank_you = $this->get_text_inline( 'Thank you so much for using our products!', 'thank-you-for-using-products' );
21272 $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' );
21273
21274 $products_and_add_ons = '';
21275 foreach ( $user_plugins as $user_plugin ) {
21276 if ( ! empty( $products_and_add_ons ) ) {
21277 $products_and_add_ons .= ', ';
21278 }
21279
21280 if ( ! $user_plugin->has_addons ) {
21281 $products_and_add_ons .= sprintf(
21282 "<span data-plugin-id='%d'>%s</span>",
21283 $user_plugin->id,
21284 $user_plugin->title
21285 );
21286 } else {
21287 $products_and_add_ons .= sprintf(
21288 "<span data-plugin-id='%d'>%s</span>",
21289 $user_plugin->id,
21290 sprintf(
21291 $this->get_text_inline( '%s and its add-ons', 'product-and-its-addons' ),
21292 $user_plugin->title
21293 )
21294 );
21295 }
21296 }
21297
21298 $multiple_products_text = sprintf(
21299 "<small class='products'><strong>%s:</strong> %s</small>",
21300 $this->get_text_inline( 'Products', 'products' ),
21301 $products_and_add_ons
21302 );
21303 }
21304
21305 $actions = sprintf(
21306 '<ul><li>%s<span class="action-description"> - %s</span></li><li>%s<span class="action-description"> - %s</span></li></ul>',
21307 sprintf('<button class="button button-primary allow-marketing">%s</button>', $this->get_text_inline( 'Yes', 'yes' ) ),
21308 $this->get_text_inline( 'send me security & feature updates, educational content and offers.', 'send-updates' ),
21309 sprintf('<button class="button button-secondary">%s</button>', $this->get_text_inline( 'No', 'no' ) ),
21310 sprintf(
21311 $this->get_text_inline( 'do %sNOT%s send me security & feature updates, educational content and offers.', 'do-not-send-updates' ),
21312 '<span class="underlined">',
21313 '</span>'
21314 )
21315 );
21316
21317 return sprintf(
21318 '%s %s %s',
21319 $thank_you,
21320 $already_opted_in,
21321 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>') .
21322 '<br><br>' .
21323 '<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>' .
21324 $actions .
21325 ( $is_single_parent_product ? '' : $multiple_products_text )
21326 );
21327 }
21328
21329 /**
21330 * This method is called for opted-in users to fetch the is_marketing_allowed flag of the user for all the
21331 * plugins and themes they've opted in to.
21332 *
21333 * @author Leo Fajardo (@leorw)
21334 * @since 2.1.0
21335 *
21336 * @param string $user_email
21337 * @param string $license_key
21338 * @param array $plugin_ids
21339 * @param string|null $license_key
21340 *
21341 * @return array|false
21342 */
21343 private function fetch_user_marketing_flag_status_by_plugins( $user_email, $license_key, $plugin_ids ) {
21344 $request = array(
21345 'method' => 'POST',
21346 'body' => array(),
21347 'timeout' => WP_FS__DEBUG_SDK ? 60 : 30,
21348 );
21349
21350 if ( is_string( $user_email ) ) {
21351 $request['body']['email'] = $user_email;
21352 } else {
21353 $request['body']['license_key'] = $license_key;
21354 }
21355
21356 $result = array();
21357
21358 $url = WP_FS__ADDRESS . '/action/service/user_plugin/';
21359 $total_plugin_ids = count( $plugin_ids );
21360
21361 $plugin_ids_count_per_request = 10;
21362 for ( $i = 1; $i <= $total_plugin_ids; $i += $plugin_ids_count_per_request ) {
21363 $plugin_ids_set = array_slice( $plugin_ids, $i - 1, $plugin_ids_count_per_request );
21364
21365 $request['body']['plugin_ids'] = $plugin_ids_set;
21366
21367 $response = self::safe_remote_post(
21368 $url,
21369 $request,
21370 WP_FS__TIME_24_HOURS_IN_SEC,
21371 WP_FS__TIME_12_HOURS_IN_SEC
21372 );
21373
21374 if ( ! is_wp_error( $response ) ) {
21375 $decoded = is_string( $response['body'] ) ?
21376 json_decode( $response['body'] ) :
21377 null;
21378
21379 if (
21380 !is_object($decoded) ||
21381 !isset($decoded->success) ||
21382 true !== $decoded->success ||
21383 !isset( $decoded->data ) ||
21384 !is_array( $decoded->data )
21385 ) {
21386 return false;
21387 }
21388
21389 $result = array_merge( $result, $decoded->data );
21390 }
21391 }
21392
21393 return $result;
21394 }
21395
21396 /**
21397 * @author Leo Fajardo (@leorw)
21398 * @since 2.1.0
21399 */
21400 function _maybe_show_gdpr_admin_notice() {
21401 if ( ! $this->is_user_in_admin() ) {
21402 return;
21403 }
21404
21405 if ( ! $this->should_handle_gdpr_admin_notice() ) {
21406 return;
21407 }
21408
21409 if ( ! $this->is_user_admin() ) {
21410 return;
21411 }
21412
21413 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
21414
21415 $lock = FS_User_Lock::instance();
21416
21417 /**
21418 * Try to acquire a 60-sec lock based on the WP user and thread/process ID.
21419 */
21420 if ( ! $lock->try_lock( 60 ) ) {
21421 return;
21422 }
21423
21424 /**
21425 * @var $current_wp_user WP_User
21426 */
21427 $current_wp_user = self::_get_current_wp_user();
21428
21429 /**
21430 * @var FS_User $current_fs_user
21431 */
21432 $current_fs_user = Freemius::_get_user_by_email( $current_wp_user->user_email );
21433
21434 $ten_years_in_sec = 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC;
21435
21436 if ( ! is_object( $current_fs_user ) ) {
21437 // 10-year lock.
21438 $lock->lock( $ten_years_in_sec );
21439
21440 return;
21441 }
21442
21443 $gdpr = FS_GDPR_Manager::instance();
21444
21445 if ( $gdpr->is_opt_in_notice_shown() ) {
21446 // 30-day lock.
21447 $lock->lock( 30 * WP_FS__TIME_24_HOURS_IN_SEC );
21448
21449 return;
21450 }
21451
21452 if ( ! $gdpr->should_show_opt_in_notice() ) {
21453 // 10-year lock.
21454 $lock->lock( $ten_years_in_sec );
21455
21456 return;
21457 }
21458
21459 $last_time_notice_shown = $gdpr->last_time_notice_was_shown();
21460 $was_notice_shown_before = ( false !== $last_time_notice_shown );
21461
21462 if ( $was_notice_shown_before &&
21463 30 * WP_FS__TIME_24_HOURS_IN_SEC > time() - $last_time_notice_shown
21464 ) {
21465 // If the notice was shown before, show it again after 30 days from the last time it was shown.
21466 return;
21467 }
21468
21469 /**
21470 * Find all plugin IDs that were installed by the current admin.
21471 */
21472 $plugin_ids_map = self::get_user_opted_in_module_ids_map( $current_fs_user->id );
21473
21474 if ( empty( $plugin_ids_map )) {
21475 $lock->lock( $ten_years_in_sec );
21476
21477 return;
21478 }
21479
21480 $user_plugins = $this->fetch_user_marketing_flag_status_by_plugins(
21481 $current_fs_user->email,
21482 null,
21483 array_keys( $plugin_ids_map )
21484 );
21485
21486 if ( empty( $user_plugins ) ) {
21487 $lock->lock(
21488 is_array($user_plugins) ?
21489 $ten_years_in_sec :
21490 // Lock for 24-hours on errors.
21491 WP_FS__TIME_24_HOURS_IN_SEC
21492 );
21493
21494 return;
21495 }
21496
21497 $has_unset_marketing_optin = false;
21498
21499 foreach ( $user_plugins as $user_plugin ) {
21500 if ( true == $user_plugin->is_marketing_allowed ) {
21501 unset( $plugin_ids_map[ $user_plugin->plugin_id ] );
21502 }
21503
21504 if ( ! $has_unset_marketing_optin && is_null( $user_plugin->is_marketing_allowed ) ) {
21505 $has_unset_marketing_optin = true;
21506 }
21507 }
21508
21509 if ( empty( $plugin_ids_map ) ||
21510 ( $was_notice_shown_before && ! $has_unset_marketing_optin )
21511 ) {
21512 $lock->lock( $ten_years_in_sec );
21513
21514 return;
21515 }
21516
21517 $modules = array_merge(
21518 array_values( self::$_accounts->get_option( 'plugins', array() ) ),
21519 array_values( self::$_accounts->get_option( 'themes', array() ) )
21520 );
21521
21522 foreach ( $modules as $module ) {
21523 if ( ! FS_Plugin::is_valid_id( $module->parent_plugin_id ) && isset( $plugin_ids_map[ $module->id ] ) ) {
21524 $plugin_ids_map[ $module->id ] = $module;
21525 }
21526 }
21527
21528 $plugin_title = null;
21529 if ( 1 === count( $plugin_ids_map ) ) {
21530 $module = reset( $plugin_ids_map );
21531 $plugin_title = $module->title;
21532 }
21533
21534 $gdpr->add_opt_in_sticky_notice(
21535 $this->get_gdpr_admin_notice_string( $plugin_ids_map ),
21536 $plugin_title
21537 );
21538
21539 $this->add_gdpr_optin_ajax_handler_and_style();
21540
21541 $gdpr->notice_was_just_shown();
21542
21543 // 30-day lock.
21544 $lock->lock( 30 * WP_FS__TIME_24_HOURS_IN_SEC );
21545 }
21546
21547 /**
21548 * Prevents the GDPR opt-in admin notice from being added if the user has already chosen to allow or not allow
21549 * marketing.
21550 *
21551 * @author Leo Fajardo (@leorw)
21552 * @since 2.1.0
21553 */
21554 private function disable_opt_in_notice_and_lock_user() {
21555 FS_GDPR_Manager::instance()->disable_opt_in_notice();
21556
21557 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
21558
21559 // 10-year lock.
21560 FS_User_Lock::instance()->lock( 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC );
21561 }
21562
21563 /**
21564 * @author Leo Fajardo (@leorw)
21565 * @since 2.1.0
21566 */
21567 function _add_gdpr_optin_js() {
21568 $vars = array( 'id' => $this->_module_id );
21569
21570 fs_require_once_template( 'gdpr-optin-js.php', $vars );
21571 }
21572
21573 /**
21574 * @author Leo Fajardo (@leorw)
21575 * @since 2.1.0
21576 */
21577 function enqueue_gdpr_optin_notice_style() {
21578 fs_enqueue_local_style( 'fs_gdpr_optin_notice', '/admin/gdpr-optin-notice.css' );
21579 }
21580
21581 /**
21582 * @author Leo Fajardo (@leorw)
21583 * @since 2.1.0
21584 */
21585 function _maybe_add_gdpr_optin_ajax_handler() {
21586 $this->add_ajax_action( 'fetch_is_marketing_required_flag_value', array( &$this, '_fetch_is_marketing_required_flag_value_ajax_action' ) );
21587
21588 if ( FS_GDPR_Manager::instance()->is_opt_in_notice_shown() ) {
21589 $this->add_gdpr_optin_ajax_handler_and_style();
21590 }
21591 }
21592
21593 /**
21594 * @author Leo Fajardo (@leorw)
21595 * @since 2.1.0
21596 */
21597 function _fetch_is_marketing_required_flag_value_ajax_action() {
21598 $this->_logger->entrance();
21599
21600 $this->check_ajax_referer( 'fetch_is_marketing_required_flag_value' );
21601
21602 $license_key = fs_request_get( 'license_key' );
21603
21604 if ( empty($license_key) ) {
21605 self::shoot_ajax_failure( $this->get_text_inline( 'License key is empty.', 'empty-license-key' ) );
21606 }
21607
21608 $user_plugins = $this->fetch_user_marketing_flag_status_by_plugins(
21609 null,
21610 $license_key,
21611 array( $this->_module_id )
21612 );
21613
21614 if ( ! is_array( $user_plugins ) ||
21615 empty($user_plugins) ||
21616 !isset($user_plugins[0]->plugin_id) ||
21617 $user_plugins[0]->plugin_id != $this->_module_id
21618 ) {
21619 /**
21620 * If faced an error or if the module ID do not match to the current module, ask for GDPR opt-in.
21621 *
21622 * @author Vova Feldman (@svovaf)
21623 */
21624 self::shoot_ajax_success( array( 'is_marketing_allowed' => null ) );
21625 }
21626
21627 self::shoot_ajax_success( array( 'is_marketing_allowed' => $user_plugins[0]->is_marketing_allowed ) );
21628 }
21629
21630 /**
21631 * @author Leo Fajardo (@leorw)
21632 * @since 2.1.0
21633 */
21634 private function add_gdpr_optin_ajax_handler_and_style() {
21635 // Add GDPR action AJAX callback.
21636 $this->add_ajax_action( 'gdpr_optin_action', array( &$this, '_gdpr_optin_ajax_action' ) );
21637
21638 add_action( 'admin_footer', array( &$this, '_add_gdpr_optin_js' ) );
21639 add_action( 'admin_enqueue_scripts', array( &$this, 'enqueue_gdpr_optin_notice_style' ) );
21640 }
21641
21642 /**
21643 * @author Leo Fajardo (@leorw)
21644 * @since 2.1.0
21645 */
21646 function _gdpr_optin_ajax_action() {
21647 $this->_logger->entrance();
21648
21649 $this->check_ajax_referer( 'gdpr_optin_action' );
21650
21651 if ( ! fs_request_has( 'is_marketing_allowed' ) || ! fs_request_has( 'plugin_ids' ) ) {
21652 self::shoot_ajax_failure();
21653 }
21654
21655 $current_wp_user = self::_get_current_wp_user();
21656
21657 $plugin_ids = fs_request_get( 'plugin_ids', array() );
21658 if ( ! is_array( $plugin_ids ) || empty( $plugin_ids ) ) {
21659 self::shoot_ajax_failure();
21660 }
21661
21662 $modules = array_merge(
21663 array_values( self::$_accounts->get_option( 'plugins', array() ) ),
21664 array_values( self::$_accounts->get_option( 'themes', array() ) )
21665 );
21666
21667 foreach ( $modules as $key => $module ) {
21668 if ( ! in_array( $module->id, $plugin_ids ) ) {
21669 unset( $modules[ $key ] );
21670 }
21671 }
21672
21673 if ( empty( $modules ) ) {
21674 self::shoot_ajax_failure();
21675 }
21676
21677 $user_api = $this->get_api_user_scope_by_user( Freemius::_get_user_by_email( $current_wp_user->user_email ) );
21678
21679 foreach ( $modules as $module ) {
21680 $user_api->call( "?plugin_id={$module->id}", 'put', array(
21681 'is_marketing_allowed' => ( true == fs_request_get_bool( 'is_marketing_allowed' ) )
21682 ) );
21683 }
21684
21685 FS_GDPR_Manager::instance()->remove_opt_in_notice();
21686
21687 require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
21688
21689 // 10-year lock.
21690 FS_User_Lock::instance()->lock( 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC );
21691
21692 self::shoot_ajax_success();
21693 }
21694
21695 /**
21696 * 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.
21697 *
21698 * @author Vova Feldman (@svovaf)
21699 * @since 2.1.0
21700 *
21701 * @return bool
21702 */
21703 private function should_handle_gdpr_admin_notice() {
21704 return $this->apply_filters(
21705 'handle_gdpr_admin_notice',
21706 // Default to false.
21707 false
21708 );
21709 }
21710
21711 #endregion
21712
21713 #----------------------------------------------------------------------------------
21714 #region Marketing
21715 #----------------------------------------------------------------------------------
21716
21717 /**
21718 * Check if current user purchased any other plugins before.
21719 *
21720 * @author Vova Feldman (@svovaf)
21721 * @since 1.0.9
21722 *
21723 * @return bool
21724 */
21725 function has_purchased_before() {
21726 // TODO: Implement has_purchased_before() method.
21727 throw new Exception( 'not implemented' );
21728 }
21729
21730 /**
21731 * Check if current user classified as an agency.
21732 *
21733 * @author Vova Feldman (@svovaf)
21734 * @since 1.0.9
21735 *
21736 * @return bool
21737 */
21738 function is_agency() {
21739 // TODO: Implement is_agency() method.
21740 throw new Exception( 'not implemented' );
21741 }
21742
21743 /**
21744 * Check if current user classified as a developer.
21745 *
21746 * @author Vova Feldman (@svovaf)
21747 * @since 1.0.9
21748 *
21749 * @return bool
21750 */
21751 function is_developer() {
21752 // TODO: Implement is_developer() method.
21753 throw new Exception( 'not implemented' );
21754 }
21755
21756 /**
21757 * Check if current user classified as a business.
21758 *
21759 * @author Vova Feldman (@svovaf)
21760 * @since 1.0.9
21761 *
21762 * @return bool
21763 */
21764 function is_business() {
21765 // TODO: Implement is_business() method.
21766 throw new Exception( 'not implemented' );
21767 }
21768
21769 #endregion
21770
21771 #----------------------------------------------------------------------------------
21772 #region Helper
21773 #----------------------------------------------------------------------------------
21774
21775 /**
21776 * If running with a secret key, assume it's the developer and show pending plans as well.
21777 *
21778 * @author Vova Feldman (@svovaf)
21779 * @since 2.1.2
21780 *
21781 * @param string $path
21782 *
21783 * @return string
21784 */
21785 function add_show_pending( $path ) {
21786 if ( ! $this->has_secret_key() ) {
21787 return $path;
21788 }
21789
21790 return $path . ( false !== strpos( $path, '?' ) ? '&' : '?' ) . 'show_pending=true';
21791 }
21792
21793 #endregion
21794 }
21795