PluginProbe
Buttonizer – Floating Menus, Sticky Buttons, & Popup Builder / 1.5.1
Buttonizer – Floating Menus, Sticky Buttons, & Popup Builder v1.5.1
3.6.0 3.5.0 trunk 1.0.10 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.6.1 1.0.7 1.0.8 1.0.9 1.1 1.1.1 1.2 1.3 1.4 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.5 1.5.1 All 110 releases
buttonizer-multifunctional-button / freemius / includes / class-freemius.php

class-freemius.php in Buttonizer – Floating Menus, Sticky Buttons, & Popup Builder 1.5.1, at freemius/includes/class-freemius.php

19,942 lines 737.7 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 1.0.0
38 *
39 * @var string
40 */
41 private $_free_plugin_basename;
42 /**
43 * @since 1.0.0
44 *
45 * @var string
46 */
47 private $_plugin_dir_path;
48 /**
49 * @since 1.0.0
50 *
51 * @var string
52 */
53 private $_plugin_dir_name;
54 /**
55 * @since 1.0.0
56 *
57 * @var string
58 */
59 private $_plugin_main_file_path;
60 /**
61 * @var string[]
62 */
63 private $_plugin_data;
64 /**
65 * @since 1.0.9
66 *
67 * @var string
68 */
69 private $_plugin_name;
70 /**
71 * @since 1.2.2
72 *
73 * @var string
74 */
75 private $_module_type;
76
77 #endregion Plugin Info
78
79 /**
80 * @since 1.0.9
81 *
82 * @var bool If false, don't turn Freemius on.
83 */
84 private $_is_on;
85
86 /**
87 * @since 1.1.3
88 *
89 * @var bool If false, don't turn Freemius on.
90 */
91 private $_is_anonymous;
92
93 /**
94 * @since 1.0.9
95 * @var bool If false, issues with connectivity to Freemius API.
96 */
97 private $_has_api_connection;
98
99 /**
100 * @since 1.0.9
101 * @since 2.0.0 Default to true since we need the property during the instance construction, prior to the dynamic_init() execution.
102 * @var bool Hints the SDK if plugin can support anonymous mode (if skip connect is visible).
103 */
104 private $_enable_anonymous = true;
105
106 /**
107 * @since 1.1.7.5
108 * @var bool Hints the SDK if plugin should run in anonymous mode (only adds feedback form).
109 */
110 private $_anonymous_mode;
111
112 /**
113 * @since 1.1.9
114 * @var bool Hints the SDK if plugin have any free plans.
115 */
116 private $_is_premium_only;
117
118 /**
119 * @since 1.2.1.6
120 * @var bool Hints the SDK if plugin have premium code version at all.
121 */
122 private $_has_premium_version;
123
124 /**
125 * @since 1.2.1.6
126 * @var bool Hints the SDK if plugin should ignore pending mode by simulating a skip.
127 */
128 private $_ignore_pending_mode;
129
130 /**
131 * @since 1.0.8
132 * @var bool Hints the SDK if the plugin has any paid plans.
133 */
134 private $_has_paid_plans;
135
136 /**
137 * @since 1.2.1.5
138 * @var int Hints the SDK if the plugin offers a trial period. If negative, no trial, if zero - has a trial but
139 * without a specified period, if positive - the number of trial days.
140 */
141 private $_trial_days = - 1;
142
143 /**
144 * @since 1.2.1.5
145 * @var bool Hints the SDK if the trial requires a payment method or not.
146 */
147 private $_is_trial_require_payment = false;
148
149 /**
150 * @since 1.0.7
151 * @var bool Hints the SDK if the plugin is WordPress.org compliant.
152 */
153 private $_is_org_compliant;
154
155 /**
156 * @since 1.0.7
157 * @var bool Hints the SDK if the plugin is has add-ons.
158 */
159 private $_has_addons;
160
161 /**
162 * @since 1.1.6
163 * @var string[]bool.
164 */
165 private $_permissions;
166
167 /**
168 * @var FS_Storage
169 */
170 private $_storage;
171
172 /**
173 * @since 1.2.2.7
174 * @var FS_Cache_Manager
175 */
176 private $_cache;
177
178 /**
179 * @since 1.0.0
180 *
181 * @var FS_Logger
182 */
183 private $_logger;
184 /**
185 * @since 1.0.4
186 *
187 * @var FS_Plugin
188 */
189 private $_plugin = false;
190 /**
191 * @since 1.0.4
192 *
193 * @var FS_Plugin|false
194 */
195 private $_parent_plugin = false;
196 /**
197 * @since 1.1.1
198 *
199 * @var Freemius
200 */
201 private $_parent = false;
202 /**
203 * @since 1.0.1
204 *
205 * @var FS_User
206 */
207 private $_user = false;
208 /**
209 * @since 1.0.1
210 *
211 * @var FS_Site
212 */
213 private $_site = false;
214 /**
215 * @since 1.0.1
216 *
217 * @var FS_Plugin_License
218 */
219 private $_license;
220 /**
221 * @since 1.0.2
222 *
223 * @var FS_Plugin_Plan[]
224 */
225 private $_plans = false;
226 /**
227 * @var FS_Plugin_License[]
228 * @since 1.0.5
229 */
230 private $_licenses = false;
231
232 /**
233 * @since 1.0.1
234 *
235 * @var FS_Admin_Menu_Manager
236 */
237 private $_menu;
238
239 /**
240 * @var FS_Admin_Notices
241 */
242 private $_admin_notices;
243
244 /**
245 * @since 1.1.6
246 *
247 * @var FS_Admin_Notices
248 */
249 private static $_global_admin_notices;
250
251 /**
252 * @var FS_Logger
253 * @since 1.0.0
254 */
255 private static $_static_logger;
256
257 /**
258 * @var FS_Options
259 * @since 1.0.2
260 */
261 private static $_accounts;
262
263 /**
264 * @since 1.2.2
265 *
266 * @var number
267 */
268 private $_module_id;
269
270 /**
271 * @var Freemius[]
272 */
273 private static $_instances = array();
274
275 /**
276 * @since 1.2.3
277 *
278 * @var FS_Affiliate
279 */
280 private $affiliate = null;
281
282 /**
283 * @since 1.2.3
284 *
285 * @var FS_AffiliateTerms
286 */
287 private $plugin_affiliate_terms = null;
288
289 /**
290 * @since 1.2.3
291 *
292 * @var FS_AffiliateTerms
293 */
294 private $custom_affiliate_terms = null;
295
296 /**
297 * @since 2.0.0
298 *
299 * @var bool
300 */
301 private $_is_multisite_integrated;
302
303 /**
304 * @since 2.0.0
305 *
306 * @var bool True if the current request is for a network admin screen and the plugin is network active.
307 */
308 private $_is_network_active;
309
310 /**
311 * @since 2.0.0
312 *
313 * @var int|null The original blog ID the plugin was loaded with.
314 */
315 private $_blog_id = null;
316
317 /**
318 * @since 2.0.0
319 *
320 * @var int|null The current execution context. When true, run on network context. When int, run on the specified blog context.
321 */
322 private $_context_is_network_or_blog_id = null;
323
324 /**
325 * @since 2.0.0
326 *
327 * @var string
328 */
329 private $_dynamically_added_top_level_page_hook_name = '';
330
331 #region Uninstall Reasons IDs
332
333 const REASON_NO_LONGER_NEEDED = 1;
334 const REASON_FOUND_A_BETTER_PLUGIN = 2;
335 const REASON_NEEDED_FOR_A_SHORT_PERIOD = 3;
336 const REASON_BROKE_MY_SITE = 4;
337 const REASON_SUDDENLY_STOPPED_WORKING = 5;
338 const REASON_CANT_PAY_ANYMORE = 6;
339 const REASON_OTHER = 7;
340 const REASON_DIDNT_WORK = 8;
341 const REASON_DONT_LIKE_TO_SHARE_MY_INFORMATION = 9;
342 const REASON_COULDNT_MAKE_IT_WORK = 10;
343 const REASON_GREAT_BUT_NEED_SPECIFIC_FEATURE = 11;
344 const REASON_NOT_WORKING = 12;
345 const REASON_NOT_WHAT_I_WAS_LOOKING_FOR = 13;
346 const REASON_DIDNT_WORK_AS_EXPECTED = 14;
347 const REASON_TEMPORARY_DEACTIVATION = 15;
348
349 #endregion
350
351 /* Ctor
352 ------------------------------------------------------------------------------------------------------------------*/
353
354 /**
355 * Main singleton instance.
356 *
357 * @author Vova Feldman (@svovaf)
358 * @since 1.0.0
359 *
360 * @param number $module_id
361 * @param string|bool $slug
362 * @param bool $is_init Since 1.2.1 Is initiation sequence.
363 */
364 private function __construct( $module_id, $slug = false, $is_init = false ) {
365 if ( $is_init && is_numeric( $module_id ) && is_string( $slug ) ) {
366 $this->store_id_slug_type_path_map( $module_id, $slug );
367 }
368
369 $this->_module_id = $module_id;
370 $this->_slug = $this->get_slug();
371 $this->_module_type = $this->get_module_type();
372
373 $this->_blog_id = is_multisite() ? get_current_blog_id() : null;
374
375 $this->_storage = FS_Storage::instance( $this->_module_type, $this->_slug );
376
377 $this->_cache = FS_Cache_Manager::get_manager( WP_FS___OPTION_PREFIX . "cache_{$module_id}" );
378
379 $this->_logger = FS_Logger::get_logger( WP_FS__SLUG . '_' . $this->get_unique_affix(), WP_FS__DEBUG_SDK, WP_FS__ECHO_DEBUG_SDK );
380
381 $this->_plugin_main_file_path = $this->_find_caller_plugin_file( $is_init );
382 $this->_plugin_dir_path = plugin_dir_path( $this->_plugin_main_file_path );
383 $this->_plugin_basename = $this->get_plugin_basename();
384 $this->_free_plugin_basename = str_replace( '-premium/', '/', $this->_plugin_basename );
385
386 $this->_is_multisite_integrated = (
387 defined( "WP_FS__PRODUCT_{$module_id}_MULTISITE" ) &&
388 ( true === constant( "WP_FS__PRODUCT_{$module_id}_MULTISITE" ) )
389 );
390
391 $this->_is_network_active = (
392 is_multisite() &&
393 $this->_is_multisite_integrated &&
394 // Themes are always network activated, but the ACTUAL activation is per site.
395 $this->is_plugin() &&
396 ( is_plugin_active_for_network( $this->_plugin_basename ) ||
397 // Plugin network level activation or uninstall.
398 is_plugin_inactive( $this->_plugin_basename ) )
399 );
400
401 $this->_storage->set_network_active(
402 $this->_is_network_active,
403 $this->is_delegated_connection()
404 );
405
406 #region Migration
407
408 if ( is_multisite() ) {
409 /**
410 * If the install_timestamp exists on the site level but doesn't exist on the
411 * network level storage, it means that we need to process the storage with migration.
412 *
413 * 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.
414 *
415 * @author Vova Feldman (@svovaf)
416 * @since 2.0.0
417 */
418 if ( false === $this->_storage->get( 'install_timestamp', false, true ) &&
419 false !== $this->_storage->get( 'install_timestamp', false, false )
420 ) {
421 // Initiate storage migration.
422 $this->_storage->migrate_to_network();
423
424 // Migrate module cache to network level storage.
425 $this->_cache->migrate_to_network();
426 }
427 }
428
429 #endregion
430
431 $base_name_split = explode( '/', $this->_plugin_basename );
432 $this->_plugin_dir_name = $base_name_split[0];
433
434 if ( $this->_logger->is_on() ) {
435 $this->_logger->info( 'plugin_main_file_path = ' . $this->_plugin_main_file_path );
436 $this->_logger->info( 'plugin_dir_path = ' . $this->_plugin_dir_path );
437 $this->_logger->info( 'plugin_basename = ' . $this->_plugin_basename );
438 $this->_logger->info( 'free_plugin_basename = ' . $this->_free_plugin_basename );
439 $this->_logger->info( 'plugin_dir_name = ' . $this->_plugin_dir_name );
440 }
441
442 // Remember link between file to slug.
443 $this->store_file_slug_map();
444
445 // Store plugin's initial install timestamp.
446 if ( ! isset( $this->_storage->install_timestamp ) ) {
447 $this->_storage->install_timestamp = WP_FS__SCRIPT_START_TIME;
448 }
449
450 if ( ! is_object( $this->_plugin ) ) {
451 $this->_plugin = FS_Plugin_Manager::instance( $this->_module_id )->get();
452 }
453
454 $this->_admin_notices = FS_Admin_Notices::instance(
455 $this->_slug . ( $this->is_theme() ? ':theme' : '' ),
456 /**
457 * Ensure that the admin notice will always have a title by using the stored plugin title if available and
458 * retrieving the title via the "get_plugin_name" method if there is no stored plugin title available.
459 *
460 * @author Leo Fajardo (@leorw)
461 * @since 1.2.2
462 */
463 ( is_object( $this->_plugin ) ? $this->_plugin->title : $this->get_plugin_name() ),
464 $this->get_unique_affix()
465 );
466
467 if ( 'true' === fs_request_get( 'fs_clear_api_cache' ) ||
468 'true' === fs_request_is_action( 'restart_freemius' )
469 ) {
470 FS_Api::clear_cache();
471 $this->_cache->clear();
472 }
473
474 $this->_register_hooks();
475
476 /**
477 * Starting from version 2.0.0, `FS_Site` entities no longer have the `plan` property and have `plan_id`
478 * instead. This should be called before calling `_load_account()`, otherwise, `$this->_site` will not be
479 * loaded in `_load_account` for versions of SDK starting from 2.0.0.
480 *
481 * @author Leo Fajardo (@leorw)
482 */
483 self::migrate_install_plan_to_plan_id( $this->_storage );
484
485 $this->_load_account();
486
487 $this->_version_updates_handler();
488 }
489
490 /**
491 * Checks whether this module has a settings menu.
492 *
493 * @author Leo Fajardo (@leorw)
494 * @since 1.2.2
495 *
496 * @return bool
497 */
498 function has_settings_menu() {
499 return ( $this->_is_network_active && fs_is_network_admin() ) ?
500 $this->_menu->has_network_menu() :
501 $this->_menu->has_menu();
502 }
503
504 /**
505 * Check if the context module is free wp.org theme.
506 *
507 * This method is helpful because:
508 * 1. wp.org themes are limited to a single submenu item,
509 * and sub-submenu items are most likely not allowed (never verified).
510 * 2. wp.org themes are not allowed to redirect the user
511 * after the theme activation, therefore, the agreed UX
512 * is showing the opt-in as a modal dialog box after
513 * activation (approved by @otto42, @emiluzelac, @greenshady, @grapplerulrich).
514 *
515 * @author Vova Feldman (@svovaf)
516 * @since 1.2.2.7
517 *
518 * @return bool
519 */
520 function is_free_wp_org_theme() {
521 return (
522 $this->is_theme() &&
523 $this->is_org_repo_compliant() &&
524 ! $this->is_premium()
525 );
526 }
527
528 /**
529 * Checks whether this a submenu item is visible.
530 *
531 * @author Vova Feldman (@svovaf)
532 * @since 1.2.2.6
533 * @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.
534 *
535 * @param string $slug
536 *
537 * @return bool
538 */
539 function is_submenu_item_visible( $slug ) {
540 if ( $this->is_admin_page( $slug ) ) {
541 /**
542 * It is the current context page, so show the submenu item
543 * so the user will have the right context page, even if it
544 * was set to hidden.
545 */
546 return true;
547 }
548
549 if ( ! $this->has_settings_menu() ) {
550 // No menu settings at all.
551 return false;
552 }
553
554 if ( $this->is_free_wp_org_theme() ) {
555 /**
556 * wp.org themes are limited to a single submenu item, and
557 * sub-submenu items are most likely not allowed (never verified).
558 */
559 return false;
560 }
561
562 return $this->_menu->is_submenu_item_visible( $slug );
563 }
564
565 /**
566 * Check if a Freemius page should be accessible via the UI.
567 *
568 * @author Vova Feldman (@svovaf)
569 * @since 1.2.2.7
570 *
571 * @param string $slug
572 *
573 * @return bool
574 */
575 function is_page_visible( $slug ) {
576 if ( $this->is_admin_page( $slug ) ) {
577 return true;
578 }
579
580 return $this->_menu->is_submenu_item_visible( $slug, true, true );
581 }
582
583 /**
584 * @author Vova Feldman (@svovaf)
585 * @since 1.0.9
586 */
587 private function _version_updates_handler() {
588 if ( ! isset( $this->_storage->sdk_version ) || $this->_storage->sdk_version != $this->version ) {
589 // Freemius version upgrade mode.
590 $this->_storage->sdk_last_version = $this->_storage->sdk_version;
591 $this->_storage->sdk_version = $this->version;
592
593 if ( empty( $this->_storage->sdk_last_version ) ||
594 version_compare( $this->_storage->sdk_last_version, $this->version, '<' )
595 ) {
596 $this->_storage->sdk_upgrade_mode = true;
597 $this->_storage->sdk_downgrade_mode = false;
598 } else {
599 $this->_storage->sdk_downgrade_mode = true;
600 $this->_storage->sdk_upgrade_mode = false;
601
602 }
603
604 $this->do_action( 'sdk_version_update', $this->_storage->sdk_last_version, $this->version );
605 }
606
607 $plugin_version = $this->get_plugin_version();
608 if ( ! isset( $this->_storage->plugin_version ) || $this->_storage->plugin_version != $plugin_version ) {
609 // Plugin version upgrade mode.
610 $this->_storage->plugin_last_version = $this->_storage->plugin_version;
611 $this->_storage->plugin_version = $plugin_version;
612
613 if ( empty( $this->_storage->plugin_last_version ) ||
614 version_compare( $this->_storage->plugin_last_version, $plugin_version, '<' )
615 ) {
616 $this->_storage->plugin_upgrade_mode = true;
617 $this->_storage->plugin_downgrade_mode = false;
618 } else {
619 $this->_storage->plugin_downgrade_mode = true;
620 $this->_storage->plugin_upgrade_mode = false;
621 }
622
623 if ( ! empty( $this->_storage->plugin_last_version ) ) {
624 // Different version of the plugin was installed before, therefore it's an update.
625 $this->_storage->is_plugin_new_install = false;
626 }
627
628 $this->do_action( 'plugin_version_update', $this->_storage->plugin_last_version, $plugin_version );
629 }
630 }
631
632 #--------------------------------------------------------------------------------
633 #region Data Migration on SDK Update
634 #--------------------------------------------------------------------------------
635
636 /**
637 * @author Vova Feldman (@svovaf)
638 * @since 1.1.5
639 *
640 * @param string $sdk_prev_version
641 * @param string $sdk_version
642 */
643 function _data_migration( $sdk_prev_version, $sdk_version ) {
644 /**
645 * @since 1.1.7.3 Fixed unwanted connectivity test cleanup.
646 */
647 if ( empty( $sdk_prev_version ) ) {
648 return;
649 }
650
651 if ( version_compare( $sdk_prev_version, '2.0.0', '<' ) &&
652 version_compare( $sdk_version, '2.0.0', '>=' )
653 ) {
654 $this->migrate_to_subscriptions_collection();
655
656 $this->consolidate_licenses();
657
658 // Clear trial_plan since it's now loaded from the plans collection when needed.
659 $this->_storage->remove( 'trial_plan', true, false );
660 }
661
662 if ( version_compare( $sdk_prev_version, '1.2.3', '<' ) &&
663 version_compare( $sdk_version, '1.2.3', '>=' )
664 ) {
665 /**
666 * Starting from version 1.2.3, paths are stored as relative paths and not absolute paths; so when upgrading to 1.2.3, make paths relative.
667 *
668 * @author Leo Fajardo (@leorw)
669 */
670 $this->make_paths_relative();
671 }
672
673 if ( version_compare( $sdk_prev_version, '1.1.5', '<' ) &&
674 version_compare( $sdk_version, '1.1.5', '>=' )
675 ) {
676 // On version 1.1.5 merged connectivity and is_on data.
677 if ( isset( $this->_storage->connectivity_test ) ) {
678 if ( ! isset( $this->_storage->is_on ) ) {
679 unset( $this->_storage->connectivity_test );
680 } else {
681 $connectivity_data = $this->_storage->connectivity_test;
682 $connectivity_data['is_active'] = $this->_storage->is_on['is_active'];
683 $connectivity_data['timestamp'] = $this->_storage->is_on['timestamp'];
684
685 // Override.
686 $this->_storage->connectivity_test = $connectivity_data;
687
688 // Remove previous structure.
689 unset( $this->_storage->is_on );
690 }
691
692 }
693 }
694 }
695
696 /**
697 * @author Leo Fajardo (@leorw)
698 * @since 2.0.0
699 *
700 * @param \FS_Storage $storage
701 * @param bool|int|null $blog_id
702 */
703 private static function migrate_install_plan_to_plan_id( FS_Storage $storage, $blog_id = null ) {
704 if ( empty( $storage->sdk_version ) ) {
705 // New installation of the plugin, no need to upgrade.
706 return;
707 }
708
709 if ( ! version_compare( $storage->sdk_version, '2.0.0', '<' ) ) {
710 // Previous version is >= 2.0.0, so no need to migrate.
711 return;
712 }
713
714 // Alias.
715 $module_type = $storage->get_module_type();
716 $module_slug = $storage->get_module_slug();
717
718 $installs = self::get_all_sites( $module_type, $blog_id );
719 $install = isset( $installs[ $module_slug ] ) ? $installs[ $module_slug ] : null;
720
721 if ( ! is_object( $install ) ) {
722 return;
723 }
724
725 if ( isset( $install->plan ) && is_object( $install->plan ) ) {
726 if ( isset( $install->plan->id ) && ! empty( $install->plan->id ) ) {
727 $install->plan_id = self::_decrypt( $install->plan->id );
728 }
729
730 unset( $install->plan );
731
732 $installs[ $module_slug ] = clone $install;
733
734 self::set_account_option_by_module(
735 $module_type,
736 'sites',
737 $installs,
738 true,
739 $blog_id
740 );
741 }
742 }
743
744 /**
745 * @author Leo Fajardo (@leorw)
746 * @since 2.0.0
747 */
748 private function migrate_to_subscriptions_collection() {
749 if ( ! is_object( $this->_site ) ) {
750 return;
751 }
752
753 if ( isset( $this->_storage->subscription ) && is_object( $this->_storage->subscription ) ) {
754 $this->_storage->subscriptions = array( $this->_storage->subscription );
755 }
756 }
757
758 /**
759 * @author Leo Fajardo (@leorw)
760 * @since 2.0.0
761 */
762 private function consolidate_licenses() {
763 $plugin_licenses = self::get_account_option( 'licenses', WP_FS__MODULE_TYPE_PLUGIN );
764 if ( isset( $plugin_licenses[ $this->_slug ] ) ) {
765 $plugin_licenses = $plugin_licenses[ $this->_slug ];
766 } else {
767 $plugin_licenses = array();
768 }
769
770 $theme_licenses = self::get_account_option( 'licenses', WP_FS__MODULE_TYPE_THEME );
771 if ( isset( $theme_licenses[ $this->_slug ] ) ) {
772 $theme_licenses = $theme_licenses[ $this->_slug ];
773 } else {
774 $theme_licenses = array();
775 }
776
777 if ( empty( $plugin_licenses ) && empty( $theme_licenses ) ) {
778 return;
779 }
780
781 $all_licenses = array();
782 $user_id_license_ids_map = array();
783
784 foreach ( $plugin_licenses as $user_id => $user_licenses ) {
785 if ( is_array( $user_licenses ) ) {
786 if ( ! isset( $user_license_ids[ $user_id ] ) ) {
787 $user_id_license_ids_map[ $user_id ] = array();
788 }
789
790 foreach ( $user_licenses as $user_license ) {
791 $all_licenses[] = $user_license;
792 $user_id_license_ids_map[ $user_id ][] = $user_license->id;
793 }
794 }
795 }
796
797 foreach ( $theme_licenses as $user_id => $user_licenses ) {
798 if ( is_array( $user_licenses ) ) {
799 if ( ! isset( $user_license_ids[ $user_id ] ) ) {
800 $user_id_license_ids_map[ $user_id ] = array();
801 }
802
803 foreach ( $user_licenses as $user_license ) {
804 $all_licenses[] = $user_license;
805 $user_id_license_ids_map[ $user_id ][] = $user_license->id;
806 }
807 }
808 }
809
810 self::store_user_id_license_ids_map(
811 $user_id_license_ids_map,
812 $this->_module_id
813 );
814
815 $this->_store_licenses( true, $this->_module_id, $all_licenses );
816 }
817
818 /**
819 * Makes paths relative.
820 *
821 * @author Leo Fajardo (@leorw)
822 * @since 1.2.3
823 */
824 private function make_paths_relative() {
825 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
826
827 if ( isset( $id_slug_type_path_map[ $this->_module_id ]['path'] ) ) {
828 $id_slug_type_path_map[ $this->_module_id ]['path'] = $this->get_relative_path( $id_slug_type_path_map[ $this->_module_id ]['path'] );
829
830 self::$_accounts->set_option( 'id_slug_type_path_map', $id_slug_type_path_map, true );
831 }
832
833 if ( isset( $this->_storage->plugin_main_file ) ) {
834 $plugin_main_file = $this->_storage->plugin_main_file;
835
836 if ( isset( $plugin_main_file->path ) ) {
837 $this->_storage->plugin_main_file->path = $this->get_relative_path( $this->_storage->plugin_main_file->path );
838 } else if ( isset( $plugin_main_file->prev_path ) ) {
839 $this->_storage->plugin_main_file->prev_path = $this->get_relative_path( $this->_storage->plugin_main_file->prev_path );
840 }
841 }
842
843 // Remove invalid path that is still associated with the current slug if there's any.
844 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
845 foreach ( $file_slug_map as $plugin_basename => $slug ) {
846 if ( $slug === $this->_slug &&
847 $plugin_basename !== $this->_plugin_basename &&
848 ! file_exists( $this->get_absolute_path( $plugin_basename ) )
849 ) {
850 unset( $file_slug_map[ $plugin_basename ] );
851 self::$_accounts->set_option( 'file_slug_map', $file_slug_map, true );
852
853 break;
854 }
855 }
856 }
857
858 /**
859 * @author Vova Feldman (@svovaf)
860 * @since 1.2.2.7
861 *
862 * @param string $plugin_prev_version
863 * @param string $plugin_version
864 */
865 function _after_version_update( $plugin_prev_version, $plugin_version ) {
866 if ( $this->is_theme() ) {
867 // Expire the cache of the previous tabs since the theme may
868 // have setting updates.
869 $this->_cache->expire( 'tabs' );
870 $this->_cache->expire( 'tabs_stylesheets' );
871 }
872 }
873
874 /**
875 * A special migration logic for the $_accounts, executed for all the plugins in the system:
876 * - Moves some data to the network level storage.
877 * - If the plugin's connection was skipped for all sites, set the plugin as if it was network skipped.
878 * - If the plugin's connection was ignored for all sites, don't do anything in terms of the network connection.
879 * - 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.
880 * - 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.
881 * - 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.
882 *
883 * @author Vova Feldman (@svovaf)
884 * @since 2.0.0
885 */
886 private static function migrate_accounts_to_network() {
887 $sites = self::get_sites();
888 $sites_count = count( $sites );
889 $connection_status = array();
890 $plugin_slugs = array();
891 foreach ( $sites as $site ) {
892 $blog_id = self::get_site_blog_id( $site );
893
894 self::$_accounts->migrate_to_network( $blog_id );
895
896 /**
897 * Build a list of all Freemius powered plugins slugs.
898 */
899 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array(), $blog_id );
900 foreach ( $id_slug_type_path_map as $module_id => $data ) {
901 if ( WP_FS__MODULE_TYPE_PLUGIN === $data['type'] ) {
902 $plugin_slugs[ $data['slug'] ] = true;
903 }
904 }
905
906 $installs = self::get_account_option( 'sites', WP_FS__MODULE_TYPE_PLUGIN, $blog_id );
907
908 if ( is_array( $installs ) ) {
909 foreach ( $installs as $slug => $install ) {
910 if ( ! isset( $connection_status[ $slug ] ) ) {
911 $connection_status[ $slug ] = array();
912 }
913
914 if ( is_object( $install ) &&
915 FS_Site::is_valid_id( $install->id ) &&
916 FS_User::is_valid_id( $install->user_id )
917 ) {
918 $connection_status[ $slug ][ $blog_id ] = $install->user_id;
919 }
920 }
921 }
922 }
923
924 foreach ( $plugin_slugs as $slug => $true ) {
925 if ( ! isset( $connection_status[ $slug ] ) ) {
926 $connection_status[ $slug ] = array();
927 }
928
929 foreach ( $sites as $site ) {
930 $blog_id = self::get_site_blog_id( $site );
931
932 if ( isset( $connection_status[ $slug ][ $blog_id ] ) ) {
933 continue;
934 }
935
936 $storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
937
938 $is_anonymous = $storage->get( 'is_anonymous', null, $blog_id );
939
940 if ( ! is_null( $is_anonymous ) ) {
941 // Since 1.1.3 is_anonymous is an array.
942 if ( is_array( $is_anonymous ) && isset( $is_anonymous['is'] ) ) {
943 $is_anonymous = $is_anonymous['is'];
944 }
945
946 if ( is_bool( $is_anonymous ) && true === $is_anonymous ) {
947 $connection_status[ $slug ][ $blog_id ] = 'skipped';
948 }
949 }
950
951 if ( ! isset( $connection_status[ $slug ][ $blog_id ] ) ) {
952 $connection_status[ $slug ][ $blog_id ] = 'ignored';
953 }
954 }
955 }
956
957 $super_admins = array();
958
959 foreach ( $connection_status as $slug => $blogs_status ) {
960 $skips = 0;
961 $ignores = 0;
962 $connections = 0;
963 $opted_in_users = array();
964 $opted_in_super_admins = array();
965
966 $storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
967
968 foreach ( $blogs_status as $blog_id => $status_or_user_id ) {
969 if ( 'skipped' === $status_or_user_id ) {
970 $skips ++;
971 } else if ( 'ignored' === $status_or_user_id ) {
972 $ignores ++;
973 } else if ( FS_User::is_valid_id( $status_or_user_id ) ) {
974 $connections ++;
975
976 if ( ! isset( $opted_in_users[ $status_or_user_id ] ) ) {
977 $opted_in_users[ $status_or_user_id ] = array();
978 }
979
980 $opted_in_users[ $status_or_user_id ][] = $blog_id;
981
982 if ( isset( $super_admins[ $status_or_user_id ] ) ||
983 self::is_super_admin( $status_or_user_id )
984 ) {
985 // Cache super-admin data.
986 $super_admins[ $status_or_user_id ] = true;
987
988 // Remember opted-in super-admins for the plugin.
989 $opted_in_super_admins[ $status_or_user_id ] = true;
990 }
991 }
992 }
993
994 $main_super_admin_user_id = null;
995 $all_migrated = false;
996 if ( $sites_count == $skips ) {
997 // All sites were skipped -> network skip by copying the anonymous mode from any of the sites.
998 $storage->is_anonymous_ms = $storage->is_anonymous;
999
1000 $all_migrated = true;
1001 } else if ( $sites_count == $ignores ) {
1002 // Don't do anything, still in activation mode.
1003
1004 $all_migrated = true;
1005 } else if ( 0 < count( $opted_in_super_admins ) ) {
1006 // Find the super-admin with the majority of installs.
1007 $max_installs_by_super_admin = 0;
1008 foreach ( $opted_in_super_admins as $user_id => $true ) {
1009 $installs_count = count( $opted_in_users[ $user_id ] );
1010
1011 if ( $installs_count > $max_installs_by_super_admin ) {
1012 $max_installs_by_super_admin = $installs_count;
1013 $main_super_admin_user_id = $user_id;
1014 }
1015 }
1016
1017 if ( $sites_count == $connections && 1 == count( $opted_in_super_admins ) ) {
1018 // Super-admin opted-in for all sites in the network.
1019 $storage->is_network_connected = true;
1020
1021 $all_migrated = true;
1022 }
1023
1024 // Store network user.
1025 $storage->network_user_id = $main_super_admin_user_id;
1026
1027 $storage->network_install_blog_id = ( $sites_count == $connections ) ?
1028 // Since all sites are opted-in, associating with the main site.
1029 get_current_blog_id() :
1030 // Associating with the 1st found opted-in site.
1031 $opted_in_users[ $main_super_admin_user_id ][0];
1032
1033 /**
1034 * Make sure we migrate the plan ID of the network install, otherwise, if after the migration
1035 * the 1st page that will be loaded is the network level WP Admin and $storage->network_install_blog_id
1036 * is different than the main site of the network, the $this->_site will not be set since the plan_id
1037 * will be empty.
1038 */
1039 $storage->migrate_to_network();
1040 self::migrate_install_plan_to_plan_id( $storage, $storage->network_install_blog_id );
1041 } else {
1042 // At least one opt-in. All the opt-in were created by a non-super-admin.
1043 if ( 0 == $ignores ) {
1044 // All sites were opted-in or skipped, all by non-super-admin. So delegate all.
1045 $storage->store( 'is_delegated_connection', true, true );
1046
1047 $all_migrated = true;
1048 }
1049 }
1050
1051 if ( ! $all_migrated ) {
1052 /**
1053 * Delegate all sites that were:
1054 * 1) Opted-in by a user that is NOT the main-super-admin.
1055 * 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.
1056 */
1057 foreach ( $blogs_status as $blog_id => $status_or_user_id ) {
1058 if ( $status_or_user_id == $main_super_admin_user_id ) {
1059 continue;
1060 }
1061
1062 if ( FS_User::is_valid_id( $status_or_user_id ) ||
1063 ( 'skipped' === $status_or_user_id && is_null( $main_super_admin_user_id ) )
1064 ) {
1065 $storage->store( 'is_delegated_connection', true, $blog_id );
1066 }
1067 }
1068 }
1069
1070
1071 if ( ( $connections + $skips > 0 ) ) {
1072 if ( $ignores > 0 ) {
1073 /**
1074 * If admin already opted-in or skipped in any of the network sites, and also
1075 * have sites which the connection decision was not yet taken, set this plugin
1076 * into network activation mode so the super-admin can choose what to do with
1077 * the rest of the sites.
1078 */
1079 self::set_network_upgrade_mode( $storage );
1080 }
1081 }
1082 }
1083 }
1084
1085 /**
1086 * Set a module into network upgrade mode.
1087 *
1088 * @author Vova Feldman (@svovaf)
1089 * @since 2.0.0
1090 *
1091 * @param \FS_Storage $storage
1092 *
1093 * @return bool
1094 */
1095 private static function set_network_upgrade_mode( FS_Storage $storage ) {
1096 return $storage->is_network_activation = true;
1097 }
1098
1099 /**
1100 * Will return true after upgrading to the SDK with the network level integration,
1101 * when the super-admin involvement is required regarding the rest of the sites.
1102 *
1103 * @author Vova Feldman (@svovaf)
1104 * @since 2.0.0
1105 *
1106 * @return bool
1107 */
1108 function is_network_upgrade_mode() {
1109 return $this->_storage->get( 'is_network_activation' );
1110 }
1111
1112 /**
1113 * Clear flag after the upgrade mode completion.
1114 *
1115 * @author Vova Feldman (@svovaf)
1116 * @since 2.0.0
1117 *
1118 * @return bool True if network activation was on and now completed.
1119 */
1120 private function network_upgrade_mode_completed() {
1121 if ( fs_is_network_admin() && $this->is_network_upgrade_mode() ) {
1122 $this->_storage->remove( 'is_network_activation' );
1123
1124 return true;
1125 }
1126
1127 return false;
1128 }
1129
1130 #endregion
1131
1132 /**
1133 * This action is connected to the 'plugins_loaded' hook and helps to determine
1134 * if this is a new plugin installation or a plugin update.
1135 *
1136 * There are 3 different use-cases:
1137 * 1) New plugin installation right with Freemius:
1138 * 1.1 _activate_plugin_event_hook() will be executed first
1139 * 1.2 Since $this->_storage->is_plugin_new_install is not set,
1140 * and $this->_storage->plugin_last_version is not set,
1141 * $this->_storage->is_plugin_new_install will be set to TRUE.
1142 * 1.3 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install will
1143 * be already set to TRUE.
1144 *
1145 * 2) Plugin update, didn't have Freemius before, and now have the SDK:
1146 * 2.1 _activate_plugin_event_hook() will not be executed, because
1147 * the activation hook do NOT fires on updates since WP 3.1.
1148 * 2.2 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install will
1149 * be empty, therefore, it will be set to FALSE.
1150 *
1151 * 3) Plugin update, had Freemius in prev version as well:
1152 * 3.1 _version_updates_handler() will be executed 1st, since FS was installed
1153 * before, $this->_storage->plugin_last_version will NOT be empty,
1154 * therefore, $this->_storage->is_plugin_new_install will be set to FALSE.
1155 * 3.2 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install is
1156 * already set, therefore, it will not be modified.
1157 *
1158 * Use-case #3 is backward compatible, #3.1 will be executed since 1.0.9.
1159 *
1160 * NOTE:
1161 * The only fallback of this mechanism is if an admin updates a plugin based on use-case #2,
1162 * and then, the next immediate PageView is the plugin's main settings page, it will not
1163 * show the opt-in right away. The reason it will happen is because Freemius execution
1164 * will be turned off till the plugin is fully loaded at least once
1165 * (till $this->_storage->was_plugin_loaded is TRUE).
1166 *
1167 * @author Vova Feldman (@svovaf)
1168 * @since 1.1.9
1169 *
1170 */
1171 function _plugins_loaded() {
1172 // Update flag that plugin was loaded with Freemius at least once.
1173 $this->_storage->was_plugin_loaded = true;
1174
1175 /**
1176 * Bug fix - only set to false when it's a plugin, due to the
1177 * execution sequence of the theme hooks and our methods, if
1178 * this will be set for themes, Freemius will always assume
1179 * it's a theme update.
1180 *
1181 * @author Vova Feldman (@svovaf)
1182 * @since 1.2.2.2
1183 */
1184 if ( $this->is_plugin() &&
1185 ! isset( $this->_storage->is_plugin_new_install )
1186 ) {
1187 $this->_storage->is_plugin_new_install = false;
1188 }
1189 }
1190
1191 /**
1192 * Add special parameter to WP admin AJAX calls so when we
1193 * process AJAX calls we can identify its source properly.
1194 *
1195 * @author Leo Fajardo (@leorw)
1196 * @since 2.0.0
1197 */
1198 static function _enrich_ajax_url() {
1199 $admin_param = is_network_admin() ?
1200 '_fs_network_admin' :
1201 '_fs_blog_admin';
1202 ?>
1203 <script type="text/javascript">
1204 (function ($) {
1205 $(document).ajaxSend(function (event, jqxhr, settings) {
1206 if (settings.url &&
1207 -1 < settings.url.indexOf('admin-ajax.php') &&
1208 ! ( settings.url.indexOf( '<?php echo $admin_param ?>' ) > 0 )
1209 ) {
1210 if (settings.url.indexOf('?') > 0) {
1211 settings.url += '&';
1212 } else {
1213 settings.url += '?';
1214 }
1215
1216 settings.url += '<?php echo $admin_param ?>=true';
1217
1218 }
1219 });
1220 })(jQuery);
1221 </script>
1222 <?php
1223 }
1224
1225 /**
1226 * @author Vova Feldman (@svovaf)
1227 * @since 1.0.9
1228 */
1229 private function _register_hooks() {
1230 $this->_logger->entrance();
1231
1232 if ( is_admin() ) {
1233 add_action( 'plugins_loaded', array( &$this, '_hook_action_links_and_register_account_hooks' ) );
1234
1235 if ( $this->is_plugin() ) {
1236 $plugin_dir = dirname( $this->_plugin_dir_path ) . '/';
1237
1238 /**
1239 * @since 1.2.2
1240 *
1241 * Hook to both free and premium version activations to support
1242 * auto deactivation on the other version activation.
1243 */
1244 register_activation_hook(
1245 $plugin_dir . $this->_free_plugin_basename,
1246 array( &$this, '_activate_plugin_event_hook' )
1247 );
1248
1249 register_activation_hook(
1250 $plugin_dir . $this->premium_plugin_basename(),
1251 array( &$this, '_activate_plugin_event_hook' )
1252 );
1253 } else {
1254 add_action( 'after_switch_theme', array( &$this, '_activate_theme_event_hook' ), 10, 2 );
1255
1256 /**
1257 * Include the required hooks to capture the theme settings' page tabs
1258 * and cache them.
1259 *
1260 * @author Vova Feldman (@svovaf)
1261 * @since 1.2.2.7
1262 */
1263 if ( ! $this->_cache->has_valid( 'tabs' ) ) {
1264 add_action( 'admin_footer', array( &$this, '_tabs_capture' ) );
1265 // Add license activation AJAX callback.
1266 $this->add_ajax_action( 'store_tabs', array( &$this, '_store_tabs_ajax_action' ) );
1267
1268 add_action( 'admin_enqueue_scripts', array( &$this, '_store_tabs_styles' ), 9999999 );
1269 }
1270
1271 add_action(
1272 'admin_footer',
1273 array( &$this, '_add_freemius_tabs' ),
1274 /**
1275 * The tabs JS code must be executed after the tabs capture logic (_tabs_capture()).
1276 * That's why the priority is 11 while the tabs capture logic is added
1277 * with priority 10.
1278 *
1279 * @author Vova Feldman (@svovaf)
1280 */
1281 11
1282 );
1283
1284 add_action( 'admin_footer', array( &$this, '_style_premium_theme' ) );
1285 }
1286
1287 /**
1288 * Part of the mechanism to identify new plugin install vs. plugin update.
1289 *
1290 * @author Vova Feldman (@svovaf)
1291 * @since 1.1.9
1292 */
1293 if ( empty( $this->_storage->was_plugin_loaded ) ) {
1294 /**
1295 * During the plugin activation (not theme), 'plugins_loaded' will be already executed
1296 * when the logic gets here since the activation logic first add the activate plugins,
1297 * then triggers 'plugins_loaded', and only then include the code of the plugin that
1298 * is activated. Which means that _plugins_loaded() will NOT be executed during the
1299 * plugin activation, and that IS intentional.
1300 *
1301 * @author Vova Feldman (@svovaf)
1302 */
1303 if ( $this->is_plugin() && $this->is_activation_mode( false ) ) {
1304 add_action( 'plugins_loaded', array( &$this, '_plugins_loaded' ) );
1305 } else {
1306 // If was activated before, then it was already loaded before.
1307 $this->_plugins_loaded();
1308 }
1309 }
1310
1311 if ( ! self::is_ajax() ) {
1312 if ( ! $this->is_addon() ) {
1313 add_action( 'init', array( &$this, '_add_default_submenu_items' ), WP_FS__LOWEST_PRIORITY );
1314 }
1315 }
1316 }
1317
1318 if ( $this->is_plugin() ) {
1319 if ( $this->_is_network_active ) {
1320 add_action( 'wpmu_new_blog', array( $this, '_after_new_blog_callback' ), 10, 6 );
1321 }
1322
1323 register_deactivation_hook( $this->_plugin_main_file_path, array( &$this, '_deactivate_plugin_hook' ) );
1324 }
1325
1326 if ( is_multisite() ) {
1327 add_action( 'deactivate_blog', array( &$this, '_after_site_deactivated_callback' ) );
1328 add_action( 'archive_blog', array( &$this, '_after_site_deactivated_callback' ) );
1329 add_action( 'make_spam_blog', array( &$this, '_after_site_deactivated_callback' ) );
1330 add_action( 'deleted_blog', array( &$this, '_after_site_deleted_callback' ), 10, 2 );
1331
1332 add_action( 'activate_blog', array( &$this, '_after_site_reactivated_callback' ) );
1333 add_action( 'unarchive_blog', array( &$this, '_after_site_reactivated_callback' ) );
1334 add_action( 'make_ham_blog', array( &$this, '_after_site_reactivated_callback' ) );
1335 }
1336
1337 if ( $this->is_theme() && self::is_customizer() ) {
1338 // Register customizer upsell.
1339 add_action( 'customize_register', array( &$this, '_customizer_register' ) );
1340 }
1341
1342 add_action( 'init', array( &$this, '_redirect_on_clicked_menu_link' ), WP_FS__LOWEST_PRIORITY );
1343
1344 if ( $this->is_theme() ) {
1345 add_action( 'admin_init', array( &$this, '_add_tracking_links' ) );
1346 }
1347
1348 add_action( 'admin_init', array( &$this, '_add_license_activation' ) );
1349 $this->add_ajax_action( 'update_billing', array( &$this, '_update_billing_ajax_action' ) );
1350 $this->add_ajax_action( 'start_trial', array( &$this, '_start_trial_ajax_action' ) );
1351
1352 if ( $this->_is_network_active && fs_is_network_admin() ) {
1353 $this->add_ajax_action( 'network_activate', array( &$this, '_network_activate_ajax_action' ) );
1354 }
1355
1356 $this->add_ajax_action( 'install_premium_version', array(
1357 &$this,
1358 '_install_premium_version_ajax_action'
1359 ) );
1360
1361 $this->add_ajax_action( 'submit_affiliate_application', array( &$this, '_submit_affiliate_application' ) );
1362
1363 $this->add_action( 'after_plans_sync', array( &$this, '_check_for_trial_plans' ) );
1364
1365 $this->add_action( 'sdk_version_update', array( &$this, '_data_migration' ), WP_FS__DEFAULT_PRIORITY, 2 );
1366 $this->add_action(
1367 'plugin_version_update',
1368 array( &$this, '_after_version_update' ),
1369 WP_FS__DEFAULT_PRIORITY,
1370 2
1371 );
1372 $this->add_filter( 'after_code_type_change', array( &$this, '_after_code_type_change' ) );
1373
1374 add_action( 'admin_init', array( &$this, '_add_trial_notice' ) );
1375 add_action( 'admin_init', array( &$this, '_add_affiliate_program_notice' ) );
1376 add_action( 'admin_init', array( &$this, '_enqueue_common_css' ) );
1377
1378 /**
1379 * Handle request to reset anonymous mode for `get_reconnect_url()`.
1380 *
1381 * @author Vova Feldman (@svovaf)
1382 * @since 1.2.1.5
1383 */
1384 if ( fs_request_is_action( 'reset_anonymous_mode' ) &&
1385 $this->get_unique_affix() === fs_request_get( 'fs_unique_affix' )
1386 ) {
1387 add_action( 'admin_init', array( &$this, 'connect_again' ) );
1388 }
1389 }
1390
1391 /**
1392 * Keeping the uninstall hook registered for free or premium plugin version may result to a fatal error that
1393 * could happen when a user tries to uninstall either version while one of them is still active. Uninstalling a
1394 * plugin will trigger inclusion of the free or premium version and if one of them is active during the
1395 * uninstallation, a fatal error may occur in case the plugin's class or functions are already defined.
1396 *
1397 * @author Leo Fajardo (@leorw)
1398 *
1399 * @since 1.2.0
1400 */
1401 private function unregister_uninstall_hook() {
1402 $uninstallable_plugins = (array) get_option( 'uninstall_plugins' );
1403 unset( $uninstallable_plugins[ $this->_free_plugin_basename ] );
1404 unset( $uninstallable_plugins[ $this->premium_plugin_basename() ] );
1405
1406 update_option( 'uninstall_plugins', $uninstallable_plugins );
1407 }
1408
1409 /**
1410 * @since 1.2.0 Invalidate module's main file cache, otherwise, FS_Plugin_Updater will not fetch updates.
1411 */
1412 private function clear_module_main_file_cache() {
1413 if ( ! isset( $this->_storage->plugin_main_file ) ||
1414 empty( $this->_storage->plugin_main_file->path )
1415 ) {
1416 return;
1417 }
1418
1419 $plugin_main_file = clone $this->_storage->plugin_main_file;
1420
1421 // Store cached path (2nd layer cache).
1422 $plugin_main_file->prev_path = $plugin_main_file->path;
1423
1424 // Clear cached path.
1425 unset( $plugin_main_file->path );
1426
1427 $this->_storage->plugin_main_file = $plugin_main_file;
1428
1429 /**
1430 * Clear global cached path.
1431 *
1432 * @author Leo Fajardo (@leorw)
1433 * @since 1.2.2
1434 */
1435 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map' );
1436 unset( $id_slug_type_path_map[ $this->_module_id ]['path'] );
1437 self::$_accounts->set_option( 'id_slug_type_path_map', $id_slug_type_path_map, true );
1438 }
1439
1440 /**
1441 * @author Leo Fajardo (@leorw)
1442 * @since 2.0.0
1443 */
1444 function _hook_action_links_and_register_account_hooks() {
1445 add_action( 'admin_init', array( &$this, '_add_tracking_links' ) );
1446
1447 if ( self::is_plugins_page() && $this->is_plugin() ) {
1448 $this->hook_plugin_action_links();
1449 }
1450
1451 $this->_register_account_hooks();
1452 }
1453
1454 /**
1455 * @author Vova Feldman (@svovaf)
1456 * @since 1.0.9
1457 */
1458 private function _register_account_hooks() {
1459 if ( ! is_admin() ) {
1460 return;
1461 }
1462
1463 /**
1464 * Always show the deactivation feedback form since we added
1465 * automatic free version deactivation upon premium code activation.
1466 *
1467 * @since 1.2.1.6
1468 */
1469 $this->add_ajax_action(
1470 'submit_uninstall_reason',
1471 array( &$this, '_submit_uninstall_reason_action' )
1472 );
1473
1474 if ( ! $this->is_addon() || $this->is_parent_plugin_installed() ) {
1475 if ( ( $this->is_plugin() && self::is_plugins_page() ) ||
1476 ( $this->is_theme() && self::is_themes_page() )
1477 ) {
1478 add_action( 'admin_footer', array( &$this, '_add_deactivation_feedback_dialog_box' ) );
1479 }
1480 }
1481 }
1482
1483 /**
1484 * Leverage backtrace to find caller plugin file path.
1485 *
1486 * @author Vova Feldman (@svovaf)
1487 * @since 1.0.6
1488 *
1489 * @param bool $is_init Is initiation sequence.
1490 *
1491 * @return string
1492 */
1493 private function _find_caller_plugin_file( $is_init = false ) {
1494 // Try to load the cached value of the file path.
1495 if ( isset( $this->_storage->plugin_main_file ) ) {
1496 $plugin_main_file = $this->_storage->plugin_main_file;
1497 if ( isset( $plugin_main_file->path ) ) {
1498 $absolute_path = $this->get_absolute_path( $plugin_main_file->path );
1499 if ( file_exists( $absolute_path ) ) {
1500 return $absolute_path;
1501 }
1502 }
1503 }
1504
1505 /**
1506 * @since 1.2.1
1507 *
1508 * `clear_module_main_file_cache()` is clearing the plugin's cached path on
1509 * deactivation. Therefore, if any plugin/theme was initiating `Freemius`
1510 * with that plugin's slug, it was overriding the empty plugin path with a wrong path.
1511 *
1512 * So, we've added a special mechanism with a 2nd layer of cache that uses `prev_path`
1513 * when the class instantiator isn't the module.
1514 */
1515 if ( ! $is_init ) {
1516 // Fetch prev path cache.
1517 if ( isset( $this->_storage->plugin_main_file ) &&
1518 isset( $this->_storage->plugin_main_file->prev_path )
1519 ) {
1520 $absolute_path = $this->get_absolute_path( $this->_storage->plugin_main_file->prev_path );
1521 if ( file_exists( $absolute_path ) ) {
1522 return $absolute_path;
1523 }
1524 }
1525
1526 wp_die(
1527 $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' ) .
1528 " Module: {$this->_slug}; SDK: " . WP_FS__SDK_VERSION . ";",
1529 $this->get_text_inline( 'Error', 'error' ),
1530 array( 'back_link' => true )
1531 );
1532 }
1533
1534 /**
1535 * @since 1.2.1
1536 *
1537 * Only the original instantiator that calls dynamic_init can modify the module's path.
1538 */
1539 // Find caller module.
1540 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
1541 $this->_storage->plugin_main_file = (object) array(
1542 'path' => $id_slug_type_path_map[ $this->_module_id ]['path'],
1543 );
1544
1545 return $this->get_absolute_path( $id_slug_type_path_map[ $this->_module_id ]['path'] );
1546 }
1547
1548 /**
1549 * @author Leo Fajardo (@leorw)
1550 * @since 1.2.3
1551 *
1552 * @param string $path
1553 *
1554 * @return string
1555 */
1556 private function get_relative_path( $path ) {
1557 $module_root_dir = $this->get_module_root_dir_path();
1558 if ( 0 === strpos( $path, $module_root_dir ) ) {
1559 $path = substr( $path, strlen( $module_root_dir ) );
1560 }
1561
1562 return $path;
1563 }
1564
1565 /**
1566 * @author Leo Fajardo (@leorw)
1567 * @since 1.2.3
1568 *
1569 * @param string $path
1570 * @param string|bool $module_type
1571 *
1572 * @return string
1573 */
1574 private function get_absolute_path( $path, $module_type = false ) {
1575 $module_root_dir = $this->get_module_root_dir_path( $module_type );
1576 if ( 0 !== strpos( $path, $module_root_dir ) ) {
1577 $path = fs_normalize_path( $module_root_dir . $path );
1578 }
1579
1580 return $path;
1581 }
1582
1583 /**
1584 * @author Leo Fajardo (@leorw)
1585 * @since 1.2.3
1586 *
1587 * @param string|bool $module_type
1588 *
1589 * @return string
1590 */
1591 private function get_module_root_dir_path( $module_type = false ) {
1592 $is_plugin = empty( $module_type ) ?
1593 $this->is_plugin() :
1594 ( WP_FS__MODULE_TYPE_PLUGIN === $module_type );
1595
1596 return fs_normalize_path( trailingslashit( $is_plugin ?
1597 WP_PLUGIN_DIR :
1598 get_theme_root() ) );
1599 }
1600
1601 /**
1602 * @author Leo Fajardo (@leorw)
1603 *
1604 * @param number $module_id
1605 * @param string $slug
1606 *
1607 * @since 1.2.2
1608 */
1609 private function store_id_slug_type_path_map( $module_id, $slug ) {
1610 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
1611
1612 $store_option = false;
1613
1614 if ( ! isset( $id_slug_type_path_map[ $module_id ] ) ) {
1615 $id_slug_type_path_map[ $module_id ] = array(
1616 'slug' => $slug
1617 );
1618
1619 $store_option = true;
1620 }
1621
1622 if ( ! isset( $id_slug_type_path_map[ $module_id ]['path'] ) ||
1623 /**
1624 * This verification is for cases when suddenly the same module
1625 * is installed but with a different folder name.
1626 *
1627 * @author Vova Feldman (@svovaf)
1628 * @since 1.2.3
1629 */
1630 ! file_exists( $this->get_absolute_path(
1631 $id_slug_type_path_map[ $module_id ]['path'],
1632 $id_slug_type_path_map[ $module_id ]['type']
1633 ) )
1634 ) {
1635 $caller_main_file_and_type = $this->get_caller_main_file_and_type();
1636
1637 $id_slug_type_path_map[ $module_id ]['type'] = $caller_main_file_and_type->module_type;
1638 $id_slug_type_path_map[ $module_id ]['path'] = $caller_main_file_and_type->path;
1639
1640 $store_option = true;
1641 }
1642
1643 if ( $store_option ) {
1644 self::$_accounts->set_option( 'id_slug_type_path_map', $id_slug_type_path_map, true );
1645 }
1646 }
1647
1648 /**
1649 * Identifies the caller type: plugin or theme.
1650 *
1651 * @author Leo Fajardo (@leorw)
1652 * @since 1.2.2
1653 *
1654 * @author Vova Feldman (@svovaf)
1655 * @since 1.2.2.3 Find the earliest module in the call stack that calls to the SDK. This fix is for cases when
1656 * add-ons are relying on loading the SDK from the parent module, and also allows themes including the
1657 * SDK an internal file instead of directly from functions.php.
1658 * @since 1.2.1.7 Knows how to handle cases when an add-on includes the parent module logic.
1659 */
1660 private function get_caller_main_file_and_type() {
1661 self::require_plugin_essentials();
1662
1663 $all_plugins = get_plugins();
1664 $all_plugins_paths = array();
1665
1666 // Get active plugin's main files real full names (might be symlinks).
1667 foreach ( $all_plugins as $relative_path => &$data ) {
1668 if ( false === strpos( fs_normalize_path( $relative_path ), '/' ) ) {
1669 /**
1670 * Ignore plugins that don't have a folder (e.g. Hello Dolly) since they
1671 * can't really include the SDK.
1672 *
1673 * @author Vova Feldman
1674 * @since 1.2.1.7
1675 */
1676 continue;
1677 }
1678
1679 $all_plugins_paths[] = fs_normalize_path( realpath( WP_PLUGIN_DIR . '/' . $relative_path ) );
1680 }
1681
1682 $caller_file_candidate = false;
1683 $caller_map = array();
1684 $module_type = WP_FS__MODULE_TYPE_PLUGIN;
1685 $themes_dir = fs_normalize_path( get_theme_root() );
1686
1687 for ( $i = 1, $bt = debug_backtrace(), $len = count( $bt ); $i < $len; $i ++ ) {
1688 if ( empty( $bt[ $i ]['file'] ) ) {
1689 continue;
1690 }
1691
1692 if ( $i > 1 && ! empty( $bt[ $i - 1 ]['file'] ) && $bt[ $i ]['file'] === $bt[ $i - 1 ]['file'] ) {
1693 // If file same as the prev file in the stack, skip it.
1694 continue;
1695 }
1696
1697 if ( ! empty( $bt[ $i ]['function'] ) && in_array( $bt[ $i ]['function'], array(
1698 'do_action',
1699 'apply_filter',
1700 // The string split is stupid, but otherwise, theme check
1701 // throws info notices.
1702 'requir' . 'e_once',
1703 'requir' . 'e',
1704 'includ' . 'e_once',
1705 'includ' . 'e'
1706 ) )
1707 ) {
1708 // Ignore call stack hooks and files inclusion.
1709 continue;
1710 }
1711
1712 $caller_file_path = fs_normalize_path( $bt[ $i ]['file'] );
1713
1714 if ( 'functions.php' === basename( $caller_file_path ) ) {
1715 /**
1716 * 1. Assumes that theme's starting execution file is functions.php.
1717 * 2. This complex logic fixes symlink issues (e.g. with Vargant).
1718 *
1719 * @author Vova Feldman (@svovaf)
1720 * @since 1.2.2.5
1721 */
1722
1723 if ( $caller_file_path == fs_normalize_path( realpath( trailingslashit( $themes_dir ) . basename( dirname( $caller_file_path ) ) . '/' . basename( $caller_file_path ) ) ) ) {
1724 $module_type = WP_FS__MODULE_TYPE_THEME;
1725
1726 /**
1727 * Relative path of the theme, e.g.:
1728 * `my-theme/functions.php`
1729 *
1730 * @author Leo Fajardo (@leorw)
1731 */
1732 $caller_file_candidate = basename( dirname( $caller_file_path ) ) .
1733 '/' .
1734 basename( $caller_file_path );
1735
1736 continue;
1737 }
1738 }
1739
1740 $caller_file_hash = md5( $caller_file_path );
1741
1742 if ( ! isset( $caller_map[ $caller_file_hash ] ) ) {
1743 foreach ( $all_plugins_paths as $plugin_path ) {
1744 if ( false !== strpos( $caller_file_path, fs_normalize_path( dirname( $plugin_path ) . '/' ) ) ) {
1745 $caller_map[ $caller_file_hash ] = fs_normalize_path( $plugin_path );
1746 break;
1747 }
1748 }
1749 }
1750
1751 if ( isset( $caller_map[ $caller_file_hash ] ) ) {
1752 $module_type = WP_FS__MODULE_TYPE_PLUGIN;
1753 $caller_file_candidate = plugin_basename( $caller_map[ $caller_file_hash ] );
1754 }
1755 }
1756
1757 return (object) array(
1758 'module_type' => $module_type,
1759 'path' => $caller_file_candidate
1760 );
1761 }
1762
1763 #----------------------------------------------------------------------------------
1764 #region Deactivation Feedback Form
1765 #----------------------------------------------------------------------------------
1766
1767 /**
1768 * Displays a confirmation and feedback dialog box when the user clicks on the "Deactivate" link on the plugins
1769 * page.
1770 *
1771 * @author Vova Feldman (@svovaf)
1772 * @author Leo Fajardo (@leorw)
1773 * @since 1.1.2
1774 */
1775 function _add_deactivation_feedback_dialog_box() {
1776 /* Check the type of user:
1777 * 1. Long-term (long-term)
1778 * 2. Non-registered and non-anonymous short-term (non-registered-and-non-anonymous-short-term).
1779 * 3. Short-term (short-term)
1780 */
1781 $is_long_term_user = true;
1782
1783 // Check if the site is at least 2 days old.
1784 $time_installed = $this->_storage->install_timestamp;
1785
1786 // Difference in seconds.
1787 $date_diff = time() - $time_installed;
1788
1789 // Convert seconds to days.
1790 $date_diff_days = floor( $date_diff / ( 60 * 60 * 24 ) );
1791
1792 if ( $date_diff_days < 2 ) {
1793 $is_long_term_user = false;
1794 }
1795
1796 $is_long_term_user = $this->apply_filters( 'is_long_term_user', $is_long_term_user );
1797
1798 if ( $is_long_term_user ) {
1799 $user_type = 'long-term';
1800 } else {
1801 if ( ! $this->is_registered() && ! $this->is_anonymous() ) {
1802 $user_type = 'non-registered-and-non-anonymous-short-term';
1803 } else {
1804 $user_type = 'short-term';
1805 }
1806 }
1807
1808 $uninstall_reasons = $this->_get_uninstall_reasons( $user_type );
1809
1810 // Load the HTML template for the deactivation feedback dialog box.
1811 $vars = array(
1812 'reasons' => $uninstall_reasons,
1813 'id' => $this->_module_id
1814 );
1815
1816 /**
1817 * @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.
1818 */
1819 fs_require_template( 'forms/deactivation/form.php', $vars );
1820 }
1821
1822 /**
1823 * @author Leo Fajardo (@leorw)
1824 * @since 1.1.2
1825 *
1826 * @param string $user_type
1827 *
1828 * @return array The uninstall reasons for the specified user type.
1829 */
1830 function _get_uninstall_reasons( $user_type = 'long-term' ) {
1831 $module_type = $this->_module_type;
1832
1833 $internal_message_template_var = array(
1834 'id' => $this->_module_id
1835 );
1836
1837 $plan = $this->get_plan();
1838
1839 if ( $this->is_registered() && is_object( $plan ) && $plan->has_technical_support() ) {
1840 $contact_support_template = fs_get_template( 'forms/deactivation/contact.php', $internal_message_template_var );
1841 } else {
1842 $contact_support_template = '';
1843 }
1844
1845 $reason_found_better_plugin = array(
1846 'id' => self::REASON_FOUND_A_BETTER_PLUGIN,
1847 'text' => sprintf( $this->get_text_inline( 'I found a better %s', 'reason-found-a-better-plugin' ), $module_type ),
1848 'input_type' => 'textfield',
1849 'input_placeholder' => sprintf( $this->get_text_inline( "What's the %s's name?", 'placeholder-plugin-name' ), $module_type ),
1850 );
1851
1852 $reason_temporary_deactivation = array(
1853 'id' => self::REASON_TEMPORARY_DEACTIVATION,
1854 'text' => sprintf(
1855 $this->get_text_inline( "It's a temporary %s. I'm just debugging an issue.", 'reason-temporary-x' ),
1856 strtolower( $this->is_plugin() ?
1857 $this->get_text_inline( 'Deactivation', 'deactivation' ) :
1858 $this->get_text_inline( 'Theme Switch', 'theme-switch' )
1859 )
1860 ),
1861 'input_type' => '',
1862 'input_placeholder' => ''
1863 );
1864
1865 $reason_other = array(
1866 'id' => self::REASON_OTHER,
1867 'text' => $this->get_text_inline( 'Other', 'reason-other' ),
1868 'input_type' => 'textfield',
1869 'input_placeholder' => ''
1870 );
1871
1872 $long_term_user_reasons = array(
1873 array(
1874 'id' => self::REASON_NO_LONGER_NEEDED,
1875 'text' => sprintf( $this->get_text_inline( 'I no longer need the %s', 'reason-no-longer-needed' ), $module_type ),
1876 'input_type' => '',
1877 'input_placeholder' => ''
1878 ),
1879 $reason_found_better_plugin,
1880 array(
1881 'id' => self::REASON_NEEDED_FOR_A_SHORT_PERIOD,
1882 'text' => sprintf( $this->get_text_inline( 'I only needed the %s for a short period', 'reason-needed-for-a-short-period' ), $module_type ),
1883 'input_type' => '',
1884 'input_placeholder' => ''
1885 ),
1886 array(
1887 'id' => self::REASON_BROKE_MY_SITE,
1888 'text' => sprintf( $this->get_text_inline( 'The %s broke my site', 'reason-broke-my-site' ), $module_type ),
1889 'input_type' => '',
1890 'input_placeholder' => '',
1891 'internal_message' => $contact_support_template
1892 ),
1893 array(
1894 'id' => self::REASON_SUDDENLY_STOPPED_WORKING,
1895 'text' => sprintf( $this->get_text_inline( 'The %s suddenly stopped working', 'reason-suddenly-stopped-working' ), $module_type ),
1896 'input_type' => '',
1897 'input_placeholder' => '',
1898 'internal_message' => $contact_support_template
1899 )
1900 );
1901
1902 if ( $this->is_paying() ) {
1903 $long_term_user_reasons[] = array(
1904 'id' => self::REASON_CANT_PAY_ANYMORE,
1905 'text' => $this->get_text_inline( "I can't pay for it anymore", 'reason-cant-pay-anymore' ),
1906 'input_type' => 'textfield',
1907 'input_placeholder' => $this->get_text_inline( 'What price would you feel comfortable paying?', 'placeholder-comfortable-price' )
1908 );
1909 }
1910
1911 $reason_dont_share_info = array(
1912 'id' => self::REASON_DONT_LIKE_TO_SHARE_MY_INFORMATION,
1913 'text' => $this->get_text_inline( "I don't like to share my information with you", 'reason-dont-like-to-share-my-information' ),
1914 'input_type' => '',
1915 'input_placeholder' => ''
1916 );
1917
1918 /**
1919 * If the current user has selected the "don't share data" reason in the deactivation feedback modal, inform the
1920 * user by showing additional message that he doesn't have to share data and can just choose to skip the opt-in
1921 * (the Skip button is included in the message to show). This message will only be shown if anonymous mode is
1922 * enabled and the user's account is currently not in pending activation state (similar to the way the Skip
1923 * button in the opt-in form is shown/hidden).
1924 */
1925 if ( $this->is_enable_anonymous() && ! $this->is_pending_activation() ) {
1926 $reason_dont_share_info['internal_message'] = fs_get_template( 'forms/deactivation/retry-skip.php', $internal_message_template_var );
1927 }
1928
1929 $uninstall_reasons = array(
1930 'long-term' => $long_term_user_reasons,
1931 'non-registered-and-non-anonymous-short-term' => array(
1932 array(
1933 'id' => self::REASON_DIDNT_WORK,
1934 'text' => sprintf( $this->get_text_inline( "The %s didn't work", 'reason-didnt-work' ), $module_type ),
1935 'input_type' => '',
1936 'input_placeholder' => ''
1937 ),
1938 $reason_dont_share_info,
1939 $reason_found_better_plugin
1940 ),
1941 'short-term' => array(
1942 array(
1943 'id' => self::REASON_COULDNT_MAKE_IT_WORK,
1944 'text' => $this->get_text_inline( "I couldn't understand how to make it work", 'reason-couldnt-make-it-work' ),
1945 'input_type' => '',
1946 'input_placeholder' => '',
1947 'internal_message' => $contact_support_template
1948 ),
1949 $reason_found_better_plugin,
1950 array(
1951 'id' => self::REASON_GREAT_BUT_NEED_SPECIFIC_FEATURE,
1952 '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 ),
1953 'input_type' => 'textarea',
1954 'input_placeholder' => $this->get_text_inline( 'What feature?', 'placeholder-feature' )
1955 ),
1956 array(
1957 'id' => self::REASON_NOT_WORKING,
1958 'text' => sprintf( $this->get_text_inline( 'The %s is not working', 'reason-not-working' ), $module_type ),
1959 'input_type' => 'textarea',
1960 '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' )
1961 ),
1962 array(
1963 'id' => self::REASON_NOT_WHAT_I_WAS_LOOKING_FOR,
1964 'text' => $this->get_text_inline( "It's not what I was looking for", 'reason-not-what-i-was-looking-for' ),
1965 'input_type' => 'textarea',
1966 'input_placeholder' => $this->get_text_inline( "What you've been looking for?", 'placeholder-what-youve-been-looking-for' )
1967 ),
1968 array(
1969 'id' => self::REASON_DIDNT_WORK_AS_EXPECTED,
1970 'text' => sprintf( $this->get_text_inline( "The %s didn't work as expected", 'reason-didnt-work-as-expected' ), $module_type ),
1971 'input_type' => 'textarea',
1972 'input_placeholder' => $this->get_text_inline( 'What did you expect?', 'placeholder-what-did-you-expect' )
1973 )
1974 )
1975 );
1976
1977 // Randomize the reasons for the current user type.
1978 shuffle( $uninstall_reasons[ $user_type ] );
1979
1980 // Keep the following reasons as the last items in the list.
1981 $uninstall_reasons[ $user_type ][] = $reason_temporary_deactivation;
1982 $uninstall_reasons[ $user_type ][] = $reason_other;
1983
1984 $uninstall_reasons = $this->apply_filters( 'uninstall_reasons', $uninstall_reasons );
1985
1986 return $uninstall_reasons[ $user_type ];
1987 }
1988
1989 /**
1990 * Called after the user has submitted his reason for deactivating the plugin.
1991 *
1992 * @author Leo Fajardo (@leorw)
1993 * @since 1.1.2
1994 */
1995 function _submit_uninstall_reason_action() {
1996 $this->_logger->entrance();
1997
1998 $this->check_ajax_referer( 'submit_uninstall_reason' );
1999
2000 $reason_id = fs_request_get( 'reason_id' );
2001
2002 // Check if the given reason ID is an unsigned integer.
2003 if ( ! ctype_digit( $reason_id ) ) {
2004 exit;
2005 }
2006
2007 $reason_info = trim( fs_request_get( 'reason_info', '' ) );
2008 if ( ! empty( $reason_info ) ) {
2009 $reason_info = substr( $reason_info, 0, 128 );
2010 }
2011
2012 $reason = (object) array(
2013 'id' => $reason_id,
2014 'info' => $reason_info,
2015 'is_anonymous' => fs_request_get_bool( 'is_anonymous' )
2016 );
2017
2018 $this->_storage->store( 'uninstall_reason', $reason );
2019
2020 /**
2021 * If the module type is "theme", trigger the uninstall event here (on theme deactivation) since themes do
2022 * not support uninstall hook.
2023 *
2024 * @author Leo Fajardo (@leorw)
2025 * @since 1.2.2
2026 */
2027 if ( $this->is_theme() ) {
2028 $this->_uninstall_plugin_event( false );
2029 $this->remove_sdk_reference();
2030 }
2031
2032 // Print '1' for successful operation.
2033 echo 1;
2034 exit;
2035 }
2036
2037 #endregion
2038
2039 #----------------------------------------------------------------------------------
2040 #region Instance
2041 #----------------------------------------------------------------------------------
2042
2043 /**
2044 * Main singleton instance.
2045 *
2046 * @author Vova Feldman (@svovaf)
2047 * @since 1.0.0
2048 *
2049 * @param number $module_id
2050 * @param string|bool $slug
2051 * @param bool $is_init Is initiation sequence.
2052 *
2053 * @return Freemius|false
2054 */
2055 static function instance( $module_id, $slug = false, $is_init = false ) {
2056 if ( empty( $module_id ) ) {
2057 return false;
2058 }
2059
2060 /**
2061 * 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.
2062 */
2063 self::_load_required_static();
2064
2065 if ( ! is_numeric( $module_id ) ) {
2066 if ( ! $is_init && true === $slug ) {
2067 $is_init = true;
2068 }
2069
2070 $slug = $module_id;
2071
2072 $module = FS_Plugin_Manager::instance( $slug )->get();
2073
2074 if ( is_object( $module ) ) {
2075 $module_id = $module->id;
2076 }
2077 }
2078
2079 $key = 'm_' . $module_id;
2080
2081 if ( ! isset( self::$_instances[ $key ] ) ) {
2082 self::$_instances[ $key ] = new Freemius( $module_id, $slug, $is_init );
2083 }
2084
2085 return self::$_instances[ $key ];
2086 }
2087
2088 /**
2089 * @author Vova Feldman (@svovaf)
2090 * @since 1.0.6
2091 *
2092 * @param number $addon_id
2093 *
2094 * @return bool
2095 */
2096 private static function has_instance( $addon_id ) {
2097 return isset( self::$_instances[ 'm_' . $addon_id ] );
2098 }
2099
2100 /**
2101 * @author Leo Fajardo (@leorw)
2102 * @since 1.2.2
2103 *
2104 * @param string|number $id_or_slug
2105 *
2106 * @return number|false
2107 */
2108 private static function get_module_id( $id_or_slug ) {
2109 if ( is_numeric( $id_or_slug ) ) {
2110 return $id_or_slug;
2111 }
2112
2113 foreach ( self::$_instances as $instance ) {
2114 if ( $instance->is_plugin() && ( $id_or_slug === $instance->get_slug() ) ) {
2115 return $instance->get_id();
2116 }
2117 }
2118
2119 return false;
2120 }
2121
2122 /**
2123 * @author Vova Feldman (@svovaf)
2124 * @since 1.0.6
2125 *
2126 * @param number $id
2127 *
2128 * @return false|Freemius
2129 */
2130 static function get_instance_by_id( $id ) {
2131 return isset ( self::$_instances[ 'm_' . $id ] ) ?
2132 self::$_instances[ 'm_' . $id ] :
2133 false;
2134 }
2135
2136 /**
2137 *
2138 * @author Vova Feldman (@svovaf)
2139 * @since 1.0.1
2140 *
2141 * @param $plugin_file
2142 *
2143 * @return false|Freemius
2144 */
2145 static function get_instance_by_file( $plugin_file ) {
2146 $slug = self::find_slug_by_basename( $plugin_file );
2147
2148 return ( false !== $slug ) ?
2149 self::instance( self::get_module_id( $slug ) ) :
2150 false;
2151 }
2152
2153 /**
2154 * @author Vova Feldman (@svovaf)
2155 * @since 1.0.6
2156 *
2157 * @return false|Freemius
2158 */
2159 function get_parent_instance() {
2160 return self::get_instance_by_id( $this->_plugin->parent_plugin_id );
2161 }
2162
2163 /**
2164 * @author Vova Feldman (@svovaf)
2165 * @since 1.0.6
2166 *
2167 * @param string|number $id_or_slug
2168 *
2169 * @return false|Freemius
2170 */
2171 function get_addon_instance( $id_or_slug ) {
2172 $addon_id = self::get_module_id( $id_or_slug );
2173
2174 return self::instance( $addon_id );
2175 }
2176
2177 #endregion ------------------------------------------------------------------
2178
2179 /**
2180 * @author Vova Feldman (@svovaf)
2181 * @since 1.0.6
2182 *
2183 * @return bool
2184 */
2185 function is_parent_plugin_installed() {
2186 $is_active = self::has_instance( $this->_plugin->parent_plugin_id );
2187
2188 if ( $is_active ) {
2189 return true;
2190 }
2191
2192 /**
2193 * Parent module might be a theme. If that's the case, the add-on's FS
2194 * instance will be loaded prior to the theme's FS instance, therefore,
2195 * we need to check if it's active with a "look ahead".
2196 *
2197 * @author Vova Feldman
2198 * @since 1.2.2.3
2199 */
2200 global $fs_active_plugins;
2201 if ( is_object( $fs_active_plugins ) && is_array( $fs_active_plugins->plugins ) ) {
2202 $active_theme = wp_get_theme();
2203
2204 foreach ( $fs_active_plugins->plugins as $sdk => $module ) {
2205 if ( WP_FS__MODULE_TYPE_THEME === $module->type ) {
2206 if ( $module->plugin_path == $active_theme->get_stylesheet() ) {
2207 // Parent module is a theme and it's currently active.
2208 return true;
2209 }
2210 }
2211 }
2212 }
2213
2214 return false;
2215 }
2216
2217 /**
2218 * Check if add-on parent plugin in activation mode.
2219 *
2220 * @author Vova Feldman (@svovaf)
2221 * @since 1.0.7
2222 *
2223 * @return bool
2224 */
2225 function is_parent_in_activation() {
2226 $parent_fs = $this->get_parent_instance();
2227 if ( ! is_object( $parent_fs ) ) {
2228 return false;
2229 }
2230
2231 return ( $parent_fs->is_activation_mode() );
2232 }
2233
2234 /**
2235 * Is plugin in activation mode.
2236 *
2237 * @author Vova Feldman (@svovaf)
2238 * @since 1.0.7
2239 *
2240 * @param bool $and_on
2241 *
2242 * @return bool
2243 */
2244 function is_activation_mode( $and_on = true ) {
2245 return fs_is_network_admin() ?
2246 $this->is_network_activation_mode( $and_on ) :
2247 $this->is_site_activation_mode( $and_on );
2248 }
2249
2250 /**
2251 * Is plugin in activation mode.
2252 *
2253 * @author Vova Feldman (@svovaf)
2254 * @since 1.0.7
2255 *
2256 * @param bool $and_on
2257 *
2258 * @return bool
2259 */
2260 function is_site_activation_mode( $and_on = true ) {
2261 return (
2262 ( $this->is_on() || ! $and_on ) &&
2263 ( ! $this->is_registered() ||
2264 ( $this->is_only_premium() && ! $this->has_features_enabled_license() ) ) &&
2265 ( ! $this->is_enable_anonymous() ||
2266 ( ! $this->is_anonymous() && ! $this->is_pending_activation() ) )
2267 );
2268 }
2269
2270 /**
2271 * Checks if the SDK in network activation mode.
2272 *
2273 * @author Leo Fajardo (@leorw)
2274 * @since 2.0.0
2275 *
2276 * @param bool $and_on
2277 *
2278 * @return bool
2279 */
2280 private function is_network_activation_mode( $and_on = true ) {
2281 if ( ! $this->_is_network_active ) {
2282 // Not network activated.
2283 return false;
2284 }
2285
2286 if ( $this->is_network_upgrade_mode() ) {
2287 // Special flag to enforce network activation mode to decide what to do with the sites that are not yet opted-in nor skipped.
2288 return true;
2289 }
2290
2291 if ( ! $this->is_site_activation_mode( $and_on ) ) {
2292 // 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.
2293 return false;
2294 }
2295
2296 if ( $this->is_network_delegated_connection() ) {
2297 // Super-admin delegated the connection to the site admins -> not activation mode.
2298 return false;
2299 }
2300
2301 if ( $this->is_network_anonymous() ) {
2302 // Super-admin skipped the connection network wide -> not activation mode.
2303 return false;
2304 }
2305
2306 if ( $this->is_network_registered() ) {
2307 // Super-admin connected at least one site -> not activation mode.
2308 return false;
2309 }
2310
2311 return true;
2312 }
2313
2314 /**
2315 * Check if current page is the opt-in/pending-activation page.
2316 *
2317 * @author Vova Feldman (@svovaf)
2318 * @since 1.2.1.7
2319 *
2320 * @return bool
2321 */
2322 function is_activation_page() {
2323 if ( $this->_menu->is_main_settings_page() ) {
2324 return true;
2325 }
2326
2327 if ( ! $this->is_activation_mode() ) {
2328 return false;
2329 }
2330
2331 // Check if current page is matching the activation page.
2332 return $this->is_matching_url( $this->get_activation_url() );
2333 }
2334
2335 /**
2336 * Check if URL path's are matching and that all querystring
2337 * arguments of the $sub_url exist in the $url with the same values.
2338 *
2339 * WARNING:
2340 * 1. This method doesn't check if the sub/domain are matching.
2341 * 2. Ignore case sensitivity.
2342 *
2343 * @author Vova Feldman (@svovaf)
2344 * @since 1.2.1.7
2345 *
2346 * @param string $sub_url
2347 * @param string $url If argument is not set, check if the sub_url matching the current's page URL.
2348 *
2349 * @return bool
2350 */
2351 private function is_matching_url( $sub_url, $url = '' ) {
2352 if ( empty( $url ) ) {
2353 $url = $_SERVER['REQUEST_URI'];
2354 }
2355
2356 $url = strtolower( $url );
2357 $sub_url = strtolower( $sub_url );
2358
2359 if ( parse_url( $sub_url, PHP_URL_PATH ) !== parse_url( $url, PHP_URL_PATH ) ) {
2360 // Different path - DO NOT OVERRIDE PAGE.
2361 return false;
2362 }
2363
2364 $url_params = array();
2365 parse_str( parse_url( $url, PHP_URL_QUERY ), $url_params );
2366
2367 $sub_url_params = array();
2368 parse_str( parse_url( $sub_url, PHP_URL_QUERY ), $sub_url_params );
2369
2370 foreach ( $sub_url_params as $key => $val ) {
2371 if ( ! isset( $url_params[ $key ] ) || $val != $url_params[ $key ] ) {
2372 // Not matching query string - DO NOT OVERRIDE PAGE.
2373 return false;
2374 }
2375 }
2376
2377 return true;
2378 }
2379
2380 /**
2381 * Get the basenames of all active plugins for specific blog. Including network activated plugins.
2382 *
2383 * @author Vova Feldman (@svovaf)
2384 * @since 2.0.0
2385 *
2386 * @param int $blog_id
2387 *
2388 * @return string[]
2389 */
2390 private static function get_active_plugins_basenames( $blog_id = 0 ) {
2391 if ( is_multisite() && $blog_id > 0 ) {
2392 $active_basenames = get_blog_option( $blog_id, 'active_plugins' );
2393 } else {
2394 $active_basenames = get_option( 'active_plugins' );
2395 }
2396
2397 if ( is_multisite() ) {
2398 $network_active_basenames = get_site_option( 'active_sitewide_plugins' );
2399
2400 if ( is_array( $network_active_basenames ) && ! empty( $network_active_basenames ) ) {
2401 $active_basenames = array_merge( $active_basenames, $network_active_basenames );
2402 }
2403 }
2404
2405 return $active_basenames;
2406 }
2407
2408 /**
2409 * Get collection of all active plugins. Including network activated plugins.
2410 *
2411 * @author Vova Feldman (@svovaf)
2412 * @since 1.0.9
2413 *
2414 * @param int $blog_id Since 2.0.0
2415 *
2416 * @return array[string]array
2417 */
2418 private static function get_active_plugins( $blog_id = 0 ) {
2419 self::require_plugin_essentials();
2420
2421 $active_plugin = array();
2422 $all_plugins = get_plugins();
2423 $active_plugins_basenames = self::get_active_plugins_basenames( $blog_id );
2424
2425 foreach ( $active_plugins_basenames as $plugin_basename ) {
2426 $active_plugin[ $plugin_basename ] = $all_plugins[ $plugin_basename ];
2427 }
2428
2429 return $active_plugin;
2430 }
2431
2432 /**
2433 * Get collection of all site active plugins for a specified blog.
2434 *
2435 * @author Vova Feldman (@svovaf)
2436 * @since 2.0.0
2437 *
2438 * @param int $blog_id
2439 *
2440 * @return array[string]array
2441 */
2442 private static function get_site_active_plugins( $blog_id = 0 ) {
2443 $active_basenames = ( is_multisite() && $blog_id > 0 ) ?
2444 get_blog_option( $blog_id, 'active_plugins' ) :
2445 get_option( 'active_plugins' );
2446
2447 $active = array();
2448 foreach ( $active_basenames as $basename ) {
2449 $active[ $basename ] = array(
2450 'is_active' => true,
2451 'Version' => '1.0', // Dummy version.
2452 'slug' => self::get_plugin_slug( $basename ),
2453 );
2454 }
2455
2456 return $active;
2457 }
2458
2459 /**
2460 * Get collection of all plugins with their activation status for a specified blog.
2461 *
2462 * @author Vova Feldman (@svovaf)
2463 * @since 1.1.8
2464 *
2465 * @param int $blog_id Since 2.0.0
2466 *
2467 * @return array Key is the plugin file path and the value is an array of the plugin data.
2468 */
2469 private static function get_all_plugins( $blog_id = 0 ) {
2470 self::require_plugin_essentials();
2471
2472 $all_plugins = get_plugins();
2473
2474 $active_plugins_basenames = self::get_active_plugins_basenames( $blog_id );
2475
2476 foreach ( $all_plugins as $basename => &$data ) {
2477 // By default set to inactive (next foreach update the active plugins).
2478 $data['is_active'] = false;
2479 // Enrich with plugin slug.
2480 $data['slug'] = self::get_plugin_slug( $basename );
2481 }
2482
2483 // Flag active plugins.
2484 foreach ( $active_plugins_basenames as $basename ) {
2485 if ( isset( $all_plugins[ $basename ] ) ) {
2486 $all_plugins[ $basename ]['is_active'] = true;
2487 }
2488 }
2489
2490 return $all_plugins;
2491 }
2492
2493 /**
2494 * Get collection of all plugins and if they are network level activated.
2495 *
2496 * @author Vova Feldman (@svovaf)
2497 * @since 2.0.0
2498 *
2499 * @return array Key is the plugin basename and the value is an array of the plugin data.
2500 */
2501 private static function get_network_plugins() {
2502 self::require_plugin_essentials();
2503
2504 $all_plugins = get_plugins();
2505
2506 $network_active_basenames = is_multisite() ?
2507 get_site_option( 'active_sitewide_plugins' ) :
2508 array();
2509
2510 foreach ( $all_plugins as $basename => &$data ) {
2511 // By default set to inactive (next foreach update the active plugins).
2512 $data['is_active'] = false;
2513 // Enrich with plugin slug.
2514 $data['slug'] = self::get_plugin_slug( $basename );
2515 }
2516
2517 // Flag active plugins.
2518 foreach ( $network_active_basenames as $basename ) {
2519 if ( isset( $all_plugins[ $basename ] ) ) {
2520 $all_plugins[ $basename ]['is_active'] = true;
2521 }
2522 }
2523
2524 return $all_plugins;
2525 }
2526
2527 /**
2528 * Cached result of get_site_transient( 'update_plugins' )
2529 *
2530 * @author Vova Feldman (@svovaf)
2531 * @since 1.1.8
2532 *
2533 * @var object
2534 */
2535 private static $_plugins_info;
2536
2537 /**
2538 * Helper function to get specified plugin's slug.
2539 *
2540 * @author Vova Feldman (@svovaf)
2541 * @since 1.1.8
2542 *
2543 * @param $basename
2544 *
2545 * @return string
2546 */
2547 private static function get_plugin_slug( $basename ) {
2548 if ( ! isset( self::$_plugins_info ) ) {
2549 self::$_plugins_info = get_site_transient( 'update_plugins' );
2550 }
2551
2552 $slug = '';
2553
2554 if ( is_object( self::$_plugins_info ) ) {
2555 if ( isset( self::$_plugins_info->no_update ) &&
2556 isset( self::$_plugins_info->no_update[ $basename ] ) &&
2557 ! empty( self::$_plugins_info->no_update[ $basename ]->slug )
2558 ) {
2559 $slug = self::$_plugins_info->no_update[ $basename ]->slug;
2560 } else if ( isset( self::$_plugins_info->response ) &&
2561 isset( self::$_plugins_info->response[ $basename ] ) &&
2562 ! empty( self::$_plugins_info->response[ $basename ]->slug )
2563 ) {
2564 $slug = self::$_plugins_info->response[ $basename ]->slug;
2565 }
2566 }
2567
2568 if ( empty( $slug ) ) {
2569 // Try to find slug from FS data.
2570 $slug = self::find_slug_by_basename( $basename );
2571 }
2572
2573 if ( empty( $slug ) ) {
2574 // Fallback to plugin's folder name.
2575 $slug = dirname( $basename );
2576 }
2577
2578 return $slug;
2579 }
2580
2581 private static $_statics_loaded = false;
2582
2583 /**
2584 * Load static resources.
2585 *
2586 * @author Vova Feldman (@svovaf)
2587 * @since 1.0.1
2588 */
2589 private static function _load_required_static() {
2590 if ( self::$_statics_loaded ) {
2591 return;
2592 }
2593
2594 self::$_static_logger = FS_Logger::get_logger( WP_FS__SLUG, WP_FS__DEBUG_SDK, WP_FS__ECHO_DEBUG_SDK );
2595
2596 self::$_static_logger->entrance();
2597
2598 self::$_accounts = FS_Options::instance( WP_FS__ACCOUNTS_OPTION_NAME, true );
2599
2600 if ( is_multisite() ) {
2601 /**
2602 * If the id_slug_type_path_map exists on the site level but doesn't exist on the
2603 * network level storage, it means that we need to process the storage with migration.
2604 *
2605 * 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, id_slug_type_path_map will be already set in the network level storage.
2606 *
2607 * @author Vova Feldman (@svovaf)
2608 * @since 2.0.0
2609 */
2610 if ( null === self::$_accounts->get_option( 'id_slug_type_path_map', null, true ) &&
2611 null !== self::$_accounts->get_option( 'id_slug_type_path_map', null, false )
2612 ) {
2613 self::migrate_accounts_to_network();
2614
2615 // Migrate API options from site level to network level.
2616 $api_network_options = FS_Option_Manager::get_manager( WP_FS__OPTIONS_OPTION_NAME, true, true );
2617 $api_network_options->migrate_to_network();
2618
2619 // Migrate API cache to network level storage.
2620 FS_Cache_Manager::get_manager( WP_FS__API_CACHE_OPTION_NAME )->migrate_to_network();
2621 }
2622 }
2623
2624 self::$_global_admin_notices = FS_Admin_Notices::instance( 'global' );
2625
2626 add_action( ( fs_is_network_admin() ? 'network_' : '' ) . 'admin_menu', array(
2627 'Freemius',
2628 '_add_debug_section'
2629 ) );
2630
2631 add_action( "wp_ajax_fs_toggle_debug_mode", array( 'Freemius', '_toggle_debug_mode' ) );
2632
2633 self::add_ajax_action_static( 'get_debug_log', array( 'Freemius', '_get_debug_log' ) );
2634
2635 self::add_ajax_action_static( 'get_db_option', array( 'Freemius', '_get_db_option' ) );
2636
2637 self::add_ajax_action_static( 'set_db_option', array( 'Freemius', '_set_db_option' ) );
2638
2639 if ( 0 == did_action( 'plugins_loaded' ) ) {
2640 add_action( 'plugins_loaded', array( 'Freemius', '_load_textdomain' ), 1 );
2641 }
2642
2643 add_action( 'admin_footer', array( 'Freemius', '_enrich_ajax_url' ) );
2644
2645 self::$_statics_loaded = true;
2646 }
2647
2648 #----------------------------------------------------------------------------------
2649 #region Localization
2650 #----------------------------------------------------------------------------------
2651
2652 /**
2653 * Load framework's text domain.
2654 *
2655 * @author Vova Feldman (@svovaf)
2656 * @since 1.2.1
2657 */
2658 static function _load_textdomain() {
2659 if ( ! is_admin() ) {
2660 return;
2661 }
2662
2663 global $fs_active_plugins;
2664
2665 // Works both for plugins and themes.
2666 load_plugin_textdomain(
2667 'freemius',
2668 false,
2669 $fs_active_plugins->newest->sdk_path . '/languages/'
2670 );
2671 }
2672
2673 #endregion
2674
2675 #----------------------------------------------------------------------------------
2676 #region Debugging
2677 #----------------------------------------------------------------------------------
2678
2679 /**
2680 * @author Vova Feldman (@svovaf)
2681 * @since 1.0.8
2682 */
2683 static function _add_debug_section() {
2684 if ( ! is_super_admin() ) {
2685 // Add debug page only for super-admins.
2686 return;
2687 }
2688
2689 self::$_static_logger->entrance();
2690
2691 $title = sprintf( '%s [v.%s]', fs_text_inline( 'Freemius Debug' ), WP_FS__SDK_VERSION );
2692
2693 if ( WP_FS__DEV_MODE ) {
2694 // Add top-level debug menu item.
2695 $hook = FS_Admin_Menu_Manager::add_page(
2696 $title,
2697 $title,
2698 'manage_options',
2699 'freemius',
2700 array( 'Freemius', '_debug_page_render' )
2701 );
2702 } else {
2703 // Add hidden debug page.
2704 $hook = FS_Admin_Menu_Manager::add_subpage(
2705 null,
2706 $title,
2707 $title,
2708 'manage_options',
2709 'freemius',
2710 array( 'Freemius', '_debug_page_render' )
2711 );
2712 }
2713
2714 if ( ! empty( $hook ) ) {
2715 add_action( "load-$hook", array( 'Freemius', '_debug_page_actions' ) );
2716 }
2717 }
2718
2719 /**
2720 * @author Vova Feldman (@svovaf)
2721 * @since 1.1.7.3
2722 */
2723 static function _toggle_debug_mode() {
2724 $is_on = fs_request_get( 'is_on', false, 'post' );
2725
2726 if ( fs_request_is_post() && in_array( $is_on, array( 0, 1 ) ) ) {
2727 update_option( 'fs_debug_mode', $is_on );
2728
2729 // Turn on/off storage logging.
2730 FS_Logger::_set_storage_logging( ( 1 == $is_on ) );
2731 }
2732
2733 exit;
2734 }
2735
2736 /**
2737 * @author Vova Feldman (@svovaf)
2738 * @since 1.2.1.6
2739 */
2740 static function _get_debug_log() {
2741 $logs = FS_Logger::load_db_logs(
2742 fs_request_get( 'filters', false, 'post' ),
2743 ! empty( $_POST['limit'] ) && is_numeric( $_POST['limit'] ) ? $_POST['limit'] : 200,
2744 ! empty( $_POST['offset'] ) && is_numeric( $_POST['offset'] ) ? $_POST['offset'] : 0
2745 );
2746
2747 self::shoot_ajax_success( $logs );
2748 }
2749
2750 /**
2751 * @author Vova Feldman (@svovaf)
2752 * @since 1.2.1.7
2753 */
2754 static function _get_db_option() {
2755 $option_name = fs_request_get( 'option_name' );
2756
2757 $value = get_option( $option_name );
2758
2759 $result = array(
2760 'name' => $option_name,
2761 );
2762
2763 if ( false !== $value ) {
2764 if ( ! is_string( $value ) ) {
2765 $value = json_encode( $value );
2766 }
2767
2768 $result['value'] = $value;
2769 }
2770
2771 self::shoot_ajax_success( $result );
2772 }
2773
2774 /**
2775 * @author Vova Feldman (@svovaf)
2776 * @since 1.2.1.7
2777 */
2778 static function _set_db_option() {
2779 $option_name = fs_request_get( 'option_name' );
2780 $option_value = fs_request_get( 'option_value' );
2781
2782 if ( ! empty( $option_value ) ) {
2783 update_option( $option_name, $option_value );
2784 }
2785
2786 self::shoot_ajax_success();
2787 }
2788
2789 /**
2790 * @author Vova Feldman (@svovaf)
2791 * @since 1.0.8
2792 */
2793 static function _debug_page_actions() {
2794 self::_clean_admin_content_section();
2795
2796 if ( fs_request_is_action( 'restart_freemius' ) ) {
2797 check_admin_referer( 'restart_freemius' );
2798
2799 if ( ! is_multisite() ) {
2800 // Clear accounts data.
2801 self::$_accounts->clear( null, true );
2802 } else {
2803 $sites = self::get_sites();
2804 foreach ( $sites as $site ) {
2805 $blog_id = self::get_site_blog_id( $site );
2806 self::$_accounts->clear( $blog_id, true );
2807 }
2808
2809 // Clear network level storage.
2810 self::$_accounts->clear( true, true );
2811 }
2812
2813 // Clear SDK reference cache.
2814 delete_option( 'fs_active_plugins' );
2815 } else if ( fs_request_is_action( 'clear_updates_data' ) ) {
2816 check_admin_referer( 'clear_updates_data' );
2817
2818 if ( ! is_multisite() ) {
2819 set_site_transient( 'update_plugins', null );
2820 set_site_transient( 'update_themes', null );
2821 } else {
2822 $current_blog_id = get_current_blog_id();
2823
2824 $sites = self::get_sites();
2825 foreach ( $sites as $site ) {
2826 switch_to_blog( self::get_site_blog_id( $site ) );
2827
2828 set_site_transient( 'update_plugins', null );
2829 set_site_transient( 'update_themes', null );
2830 }
2831
2832 switch_to_blog( $current_blog_id );
2833 }
2834 } else if ( fs_request_is_action( 'simulate_trial' ) ) {
2835 check_admin_referer( 'simulate_trial' );
2836
2837 $fs = freemius( fs_request_get( 'module_id' ) );
2838
2839 // Update SDK install to at least 24 hours before.
2840 $fs->_storage->install_timestamp = ( time() - WP_FS__TIME_24_HOURS_IN_SEC );
2841 // Unset the trial shown timestamp.
2842 unset( $fs->_storage->trial_promotion_shown );
2843 } else if ( fs_request_is_action( 'simulate_network_upgrade' ) ) {
2844 check_admin_referer( 'simulate_network_upgrade' );
2845
2846 $fs = freemius( fs_request_get( 'module_id' ) );
2847
2848 self::set_network_upgrade_mode( $fs->_storage );
2849 } else if ( fs_request_is_action( 'delete_install' ) ) {
2850 check_admin_referer( 'delete_install' );
2851
2852 self::_delete_site_by_slug(
2853 fs_request_get( 'slug' ),
2854 fs_request_get( 'module_type' ),
2855 true,
2856 fs_request_get( 'blog_id', null )
2857 );
2858 } else if ( fs_request_is_action( 'delete_user' ) ) {
2859 check_admin_referer( 'delete_user' );
2860
2861 self::delete_user( fs_request_get( 'user_id' ) );
2862 } else if ( fs_request_is_action( 'download_logs' ) ) {
2863 check_admin_referer( 'download_logs' );
2864
2865 $download_url = FS_Logger::download_db_logs(
2866 fs_request_get( 'filters', false, 'post' )
2867 );
2868
2869 if ( false === $download_url ) {
2870 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.' );
2871 }
2872
2873 fs_redirect( $download_url );
2874 }
2875 }
2876
2877 /**
2878 * @author Vova Feldman (@svovaf)
2879 * @since 1.0.8
2880 */
2881 static function _debug_page_render() {
2882 self::$_static_logger->entrance();
2883
2884 if ( ! is_multisite() ) {
2885 $all_plugins_installs = self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN );
2886 $all_themes_installs = self::get_all_sites( WP_FS__MODULE_TYPE_THEME );
2887 } else {
2888 $sites = self::get_sites();
2889
2890 $all_plugins_installs = array();
2891 $all_themes_installs = array();
2892
2893 foreach ( $sites as $site ) {
2894 $blog_id = self::get_site_blog_id( $site );
2895
2896 $plugins_installs = self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN, $blog_id );
2897
2898 foreach ( $plugins_installs as $slug => $install ) {
2899 if ( ! isset( $all_plugins_installs[ $slug ] ) ) {
2900 $all_plugins_installs[ $slug ] = array();
2901 }
2902
2903 $install->blog_id = $blog_id;
2904
2905 $all_plugins_installs[ $slug ][] = $install;
2906 }
2907
2908 $themes_installs = self::get_all_sites( WP_FS__MODULE_TYPE_THEME, $blog_id );
2909
2910 foreach ( $themes_installs as $slug => $install ) {
2911 if ( ! isset( $all_themes_installs[ $slug ] ) ) {
2912 $all_themes_installs[ $slug ] = array();
2913 }
2914
2915 $install->blog_id = $blog_id;
2916
2917 $all_themes_installs[ $slug ][] = $install;
2918 }
2919 }
2920 }
2921
2922 $licenses_by_module_type = self::get_all_licenses_by_module_type();
2923
2924 $vars = array(
2925 'plugin_sites' => $all_plugins_installs,
2926 'theme_sites' => $all_themes_installs,
2927 'users' => self::get_all_users(),
2928 'addons' => self::get_all_addons(),
2929 'account_addons' => self::get_all_account_addons(),
2930 'plugin_licenses' => $licenses_by_module_type[ WP_FS__MODULE_TYPE_PLUGIN ],
2931 'theme_licenses' => $licenses_by_module_type[ WP_FS__MODULE_TYPE_THEME ]
2932 );
2933
2934 fs_enqueue_local_style( 'fs_debug', '/admin/debug.css' );
2935 fs_require_once_template( 'debug.php', $vars );
2936 }
2937
2938 #endregion
2939
2940 #----------------------------------------------------------------------------------
2941 #region Connectivity Issues
2942 #----------------------------------------------------------------------------------
2943
2944 /**
2945 * Check if Freemius should be turned on for the current plugin install.
2946 *
2947 * Note:
2948 * $this->_is_on is updated in has_api_connectivity()
2949 *
2950 * @author Vova Feldman (@svovaf)
2951 * @since 1.0.9
2952 *
2953 * @return bool
2954 */
2955 function is_on() {
2956 self::$_static_logger->entrance();
2957
2958 if ( isset( $this->_is_on ) ) {
2959 return $this->_is_on;
2960 }
2961
2962 // If already installed or pending then sure it's on :)
2963 if ( $this->is_registered() || $this->is_pending_activation() ) {
2964 $this->_is_on = true;
2965
2966 return true;
2967 }
2968
2969 return false;
2970 }
2971
2972 /**
2973 * @author Vova Feldman (@svovaf)
2974 * @since 1.1.7.3
2975 *
2976 * @param bool $flush_if_no_connectivity
2977 *
2978 * @return bool
2979 */
2980 private function should_run_connectivity_test( $flush_if_no_connectivity = false ) {
2981 if ( ! isset( $this->_storage->connectivity_test ) ) {
2982 // Connectivity test was never executed, or cache was cleared.
2983 return true;
2984 }
2985
2986 if ( WP_FS__PING_API_ON_IP_OR_HOST_CHANGES ) {
2987 if ( WP_FS__IS_HTTP_REQUEST ) {
2988 if ( $_SERVER['HTTP_HOST'] != $this->_storage->connectivity_test['host'] ) {
2989 // Domain changed.
2990 return true;
2991 }
2992
2993 if ( WP_FS__REMOTE_ADDR != $this->_storage->connectivity_test['server_ip'] ) {
2994 // Server IP changed.
2995 return true;
2996 }
2997 }
2998 }
2999
3000 if ( $this->_storage->connectivity_test['is_connected'] &&
3001 $this->_storage->connectivity_test['is_active']
3002 ) {
3003 // API connected and Freemius is active - no need to run connectivity check.
3004 return false;
3005 }
3006
3007 if ( $flush_if_no_connectivity ) {
3008 /**
3009 * If explicitly asked to flush when no connectivity - do it only
3010 * if at least 10 sec passed from the last API connectivity test.
3011 */
3012 return ( isset( $this->_storage->connectivity_test['timestamp'] ) &&
3013 ( WP_FS__SCRIPT_START_TIME - $this->_storage->connectivity_test['timestamp'] ) > 10 );
3014 }
3015
3016 /**
3017 * @since 1.1.7 Don't check for connectivity on plugin downgrade.
3018 */
3019 $version = $this->get_plugin_version();
3020 if ( version_compare( $version, $this->_storage->connectivity_test['version'], '>' ) ) {
3021 // If it's a plugin version upgrade and Freemius is off or no connectivity, run connectivity test.
3022 return true;
3023 }
3024
3025 return false;
3026 }
3027
3028 /**
3029 * @author Vova Feldman (@svovaf)
3030 * @since 1.1.7.4
3031 *
3032 * @param int|null $blog_id Since 2.0.0.
3033 *
3034 * @return object|false
3035 */
3036 private function ping( $blog_id = null ) {
3037 if ( WP_FS__SIMULATE_NO_API_CONNECTIVITY ) {
3038 return false;
3039 }
3040
3041 $version = $this->get_plugin_version();
3042
3043 $is_update = $this->apply_filters( 'is_plugin_update', $this->is_plugin_update() );
3044
3045 return $this->get_api_plugin_scope()->ping(
3046 $this->get_anonymous_id( $blog_id ),
3047 array(
3048 'is_update' => json_encode( $is_update ),
3049 'version' => $version,
3050 'sdk' => $this->version,
3051 'is_admin' => json_encode( is_admin() ),
3052 'is_ajax' => json_encode( self::is_ajax() ),
3053 'is_cron' => json_encode( self::is_cron() ),
3054 'is_http' => json_encode( WP_FS__IS_HTTP_REQUEST ),
3055 )
3056 );
3057 }
3058
3059 /**
3060 * Check if there's any connectivity issue to Freemius API.
3061 *
3062 * @author Vova Feldman (@svovaf)
3063 * @since 1.0.9
3064 *
3065 * @param bool $flush_if_no_connectivity
3066 *
3067 * @return bool
3068 */
3069 function has_api_connectivity( $flush_if_no_connectivity = false ) {
3070 $this->_logger->entrance();
3071
3072 if ( isset( $this->_has_api_connection ) && ( $this->_has_api_connection || ! $flush_if_no_connectivity ) ) {
3073 return $this->_has_api_connection;
3074 }
3075
3076 if ( WP_FS__SIMULATE_NO_API_CONNECTIVITY &&
3077 isset( $this->_storage->connectivity_test ) &&
3078 true === $this->_storage->connectivity_test['is_connected']
3079 ) {
3080 unset( $this->_storage->connectivity_test );
3081 }
3082
3083 if ( ! $this->should_run_connectivity_test( $flush_if_no_connectivity ) ) {
3084 $this->_has_api_connection = $this->_storage->connectivity_test['is_connected'];
3085 /**
3086 * @since 1.1.6 During dev mode, if there's connectivity - turn Freemius on regardless the configuration.
3087 *
3088 * @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.
3089 */
3090 $this->_is_on = $this->_storage->connectivity_test['is_active'] ||
3091 $this->is_premium() ||
3092 ( WP_FS__DEV_MODE && $this->_has_api_connection && ! WP_FS__SIMULATE_FREEMIUS_OFF );
3093
3094 return $this->_has_api_connection;
3095 }
3096
3097 $pong = $this->ping();
3098 $is_connected = $this->get_api_plugin_scope()->is_valid_ping( $pong );
3099
3100 if ( ! $is_connected ) {
3101 // API failure.
3102 $this->_add_connectivity_issue_message( $pong );
3103 }
3104
3105 $this->store_connectivity_info( $pong, $is_connected );
3106
3107 return $this->_has_api_connection;
3108 }
3109
3110 /**
3111 * @author Vova Feldman (@svovaf)
3112 * @since 1.1.7.4
3113 *
3114 * @param object $pong
3115 * @param bool $is_connected
3116 */
3117 private function store_connectivity_info( $pong, $is_connected ) {
3118 $this->_logger->entrance();
3119
3120 $version = $this->get_plugin_version();
3121
3122 if ( ! $is_connected || WP_FS__SIMULATE_FREEMIUS_OFF ) {
3123 $is_active = false;
3124 } else {
3125 $is_active = ( isset( $pong->is_active ) && true == $pong->is_active );
3126 }
3127
3128 $is_active = $this->apply_filters(
3129 'is_on',
3130 $is_active,
3131 $this->is_plugin_update(),
3132 $version
3133 );
3134
3135 $this->_storage->connectivity_test = array(
3136 'is_connected' => $is_connected,
3137 'host' => $_SERVER['HTTP_HOST'],
3138 'server_ip' => WP_FS__REMOTE_ADDR,
3139 'is_active' => $is_active,
3140 'timestamp' => WP_FS__SCRIPT_START_TIME,
3141 // Last version with connectivity attempt.
3142 'version' => $version,
3143 );
3144
3145 $this->_has_api_connection = $is_connected;
3146 $this->_is_on = $is_active || ( WP_FS__DEV_MODE && $is_connected && ! WP_FS__SIMULATE_FREEMIUS_OFF );
3147 }
3148
3149 /**
3150 * Force turning Freemius on.
3151 *
3152 * @author Vova Feldman (@svovaf)
3153 * @since 1.1.8.1
3154 *
3155 * @return bool TRUE if successfully turned on.
3156 */
3157 private function turn_on() {
3158 $this->_logger->entrance();
3159
3160 if ( $this->is_on() || ! isset( $this->_storage->connectivity_test['is_active'] ) ) {
3161 return false;
3162 }
3163
3164 $updated_connectivity = $this->_storage->connectivity_test;
3165 $updated_connectivity['is_active'] = true;
3166 $updated_connectivity['timestamp'] = WP_FS__SCRIPT_START_TIME;
3167 $this->_storage->connectivity_test = $updated_connectivity;
3168
3169 $this->_is_on = true;
3170
3171 return true;
3172 }
3173
3174 /**
3175 * Anonymous and unique site identifier (Hash).
3176 *
3177 * @author Vova Feldman (@svovaf)
3178 * @since 1.1.0
3179 *
3180 * @param null|int $blog_id Since 2.0.0
3181 *
3182 * @return string
3183 */
3184 function get_anonymous_id( $blog_id = null ) {
3185 $unique_id = self::$_accounts->get_option( 'unique_id', null, $blog_id );
3186
3187 if ( empty( $unique_id ) || ! is_string( $unique_id ) ) {
3188 $key = fs_strip_url_protocol( get_site_url( $blog_id ) );
3189
3190 $secure_auth = SECURE_AUTH_KEY;
3191 if ( empty( $secure_auth ) || false !== strpos( $secure_auth, ' ' ) ) {
3192 // Protect against default auth key.
3193 $secure_auth = md5( microtime() );
3194 }
3195
3196 /**
3197 * Base the unique identifier on the WP secure authentication key. Which
3198 * turns the key into a secret anonymous identifier. This will help us
3199 * to avoid duplicate installs generation on the backend upon opt-in.
3200 *
3201 * @author Vova Feldman (@svovaf)
3202 * @since 1.2.3
3203 */
3204 $unique_id = md5( $key . $secure_auth );
3205
3206 self::$_accounts->set_option( 'unique_id', $unique_id, true, $blog_id );
3207 }
3208
3209 $this->_logger->departure( $unique_id );
3210
3211 return $unique_id;
3212 }
3213
3214 /**
3215 * @author Vova Feldman (@svovaf)
3216 * @since 1.1.7.4
3217 *
3218 * @return \WP_User
3219 */
3220 static function _get_current_wp_user() {
3221 self::require_pluggable_essentials();
3222
3223 return wp_get_current_user();
3224 }
3225
3226 /**
3227 * @author Vova Feldman (@svovaf)
3228 * @since 1.2.1.7
3229 *
3230 * @param string $email
3231 *
3232 * @return bool
3233 */
3234 static function is_valid_email( $email ) {
3235 if ( false === filter_var( $email, FILTER_VALIDATE_EMAIL ) ) {
3236 return false;
3237 }
3238
3239 $parts = explode( '@', $email );
3240
3241 if ( 2 !== count( $parts ) || empty( $parts[1] ) ) {
3242 return false;
3243 }
3244
3245 $blacklist = array(
3246 'admin.',
3247 'webmaster.',
3248 'localhost.',
3249 'dev.',
3250 'development.',
3251 'test.',
3252 'stage.',
3253 'staging.',
3254 );
3255
3256 // Make sure domain is not one of the blacklisted.
3257 foreach ( $blacklist as $invalid ) {
3258 if ( 0 === strpos( $parts[1], $invalid ) ) {
3259 return false;
3260 }
3261 }
3262
3263 // Get the UTF encoded domain name.
3264 $domain = idn_to_ascii( $parts[1] ) . '.';
3265
3266 return ( checkdnsrr( $domain, 'MX' ) || checkdnsrr( $domain, 'A' ) );
3267 }
3268
3269 /**
3270 * Generate API connectivity issue message.
3271 *
3272 * @author Vova Feldman (@svovaf)
3273 * @since 1.0.9
3274 *
3275 * @param mixed $api_result
3276 * @param bool $is_first_failure
3277 */
3278 function _add_connectivity_issue_message( $api_result, $is_first_failure = true ) {
3279 if ( ! $this->is_premium() && $this->_enable_anonymous ) {
3280 // Don't add message if it's the free version and can run anonymously.
3281 return;
3282 }
3283
3284 if ( ! function_exists( 'wp_nonce_url' ) ) {
3285 require_once ABSPATH . 'wp-includes/functions.php';
3286 }
3287
3288 $current_user = self::_get_current_wp_user();
3289 // $admin_email = get_option( 'admin_email' );
3290 $admin_email = $current_user->user_email;
3291
3292 // Aliases.
3293 $deactivate_plugin_title = $this->esc_html_inline( 'That\'s exhausting, please deactivate', 'deactivate-plugin-title' );
3294 $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' );
3295 $install_previous_title = $this->esc_html_inline( 'Let\'s try your previous version', 'install-previous-title' );
3296 $install_previous_desc = $this->esc_html_inline( 'Uninstall this version and install the previous one.', 'install-previous-desc' );
3297 $fix_issue_title = $this->esc_html_inline( 'Yes - I\'m giving you a chance to fix it', 'fix-issue-title' );
3298 $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' );
3299 /* translators: %s: product title (e.g. "Awesome Plugin" requires an access to...) */
3300 $x_requires_access_to_api = $this->esc_html_inline( '%s requires an access to our API.', 'x-requires-access-to-api' );
3301 $sysadmin_title = $this->esc_html_inline( 'I\'m a system administrator', 'sysadmin-title' );
3302 $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' );
3303
3304 $message = false;
3305 if ( is_object( $api_result ) &&
3306 isset( $api_result->error ) &&
3307 isset( $api_result->error->code )
3308 ) {
3309 switch ( $api_result->error->code ) {
3310 case 'curl_missing':
3311 $missing_methods = '';
3312 if ( is_array( $api_result->missing_methods ) &&
3313 ! empty( $api_result->missing_methods )
3314 ) {
3315 foreach ( $api_result->missing_methods as $m ) {
3316 if ( 'curl_version' === $m ) {
3317 continue;
3318 }
3319
3320 if ( ! empty( $missing_methods ) ) {
3321 $missing_methods .= ', ';
3322 }
3323
3324 $missing_methods .= sprintf( '<code>%s</code>', $m );
3325 }
3326
3327 if ( ! empty( $missing_methods ) ) {
3328 $missing_methods = sprintf(
3329 '<br><br><b>%s</b> %s',
3330 $this->esc_html_inline( 'Disabled method(s):', 'curl-disabled-methods' ),
3331 $missing_methods
3332 );
3333 }
3334 }
3335
3336 $message = sprintf(
3337 $x_requires_access_to_api . ' ' .
3338 $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' ) . ' ' .
3339 $missing_methods .
3340 ' %s',
3341 '<b>' . $this->get_plugin_name() . '</b>',
3342 sprintf(
3343 '<ol id="fs_firewall_issue_options"><li>%s</li><li>%s</li><li>%s</li></ol>',
3344 sprintf(
3345 '<a class="fs-resolve" data-type="curl" href="#"><b>%s</b></a>%s',
3346 $this->get_text_inline( 'I don\'t know what is cURL or how to install it, help me!', 'curl-missing-no-clue-title' ),
3347 ' - ' . sprintf(
3348 $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' ),
3349 '<a href="mailto:' . $admin_email . '">' . $admin_email . '</a>'
3350 )
3351 ),
3352 sprintf(
3353 '<b>%s</b> - %s',
3354 $sysadmin_title,
3355 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 ) ) )
3356 ),
3357 sprintf(
3358 '<a href="%s"><b>%s</b></a> - %s',
3359 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 ),
3360 $deactivate_plugin_title,
3361 $deactivate_plugin_desc
3362 )
3363 )
3364 );
3365 break;
3366 case 'cloudflare_ddos_protection':
3367 $message = sprintf(
3368 $x_requires_access_to_api . ' ' .
3369 $this->esc_html_inline( 'From unknown reason, CloudFlare, the firewall we use, blocks the connection.', 'cloudflare-blocks-connection-message' ) . ' ' .
3370 $happy_to_resolve_issue_asap .
3371 ' %s',
3372 '<b>' . $this->get_plugin_name() . '</b>',
3373 sprintf(
3374 '<ol id="fs_firewall_issue_options"><li>%s</li><li>%s</li><li>%s</li></ol>',
3375 sprintf(
3376 '<a class="fs-resolve" data-type="cloudflare" href="#"><b>%s</b></a>%s',
3377 $fix_issue_title,
3378 ' - ' . sprintf(
3379 $fix_issue_desc,
3380 '<a href="mailto:' . $admin_email . '">' . $admin_email . '</a>'
3381 )
3382 ),
3383 sprintf(
3384 '<a href="%s" target="_blank"><b>%s</b></a> - %s',
3385 sprintf( 'https://wordpress.org/plugins/%s/download/', $this->_slug ),
3386 $install_previous_title,
3387 $install_previous_desc
3388 ),
3389 sprintf(
3390 '<a href="%s"><b>%s</b></a> - %s',
3391 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 ),
3392 $deactivate_plugin_title,
3393 $deactivate_plugin_desc
3394 )
3395 )
3396 );
3397 break;
3398 case 'squid_cache_block':
3399 $message = sprintf(
3400 $x_requires_access_to_api . ' ' .
3401 $this->esc_html_inline( 'It looks like your server is using Squid ACL (access control lists), which blocks the connection.', 'squid-blocks-connection-message' ) .
3402 ' %s',
3403 '<b>' . $this->get_plugin_name() . '</b>',
3404 sprintf(
3405 '<ol id="fs_firewall_issue_options"><li>%s</li><li>%s</li><li>%s</li></ol>',
3406 sprintf(
3407 '<a class="fs-resolve" data-type="squid" href="#"><b>%s</b></a> - %s',
3408 $this->esc_html_inline( 'I don\'t know what is Squid or ACL, help me!', 'squid-no-clue-title' ),
3409 sprintf(
3410 $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' ),
3411 '<a href="mailto:' . $admin_email . '">' . $admin_email . '</a>'
3412 )
3413 ),
3414 sprintf(
3415 '<b>%s</b> - %s',
3416 $sysadmin_title,
3417 sprintf(
3418 $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' ),
3419 // We use a filter since the plugin might require additional API connectivity.
3420 '<b>' . implode( ', ', $this->apply_filters( 'api_domains', array(
3421 'api.freemius.com',
3422 'wp.freemius.com'
3423 ) ) ) . '</b>',
3424 $this->_module_type
3425 )
3426 ),
3427 sprintf(
3428 '<a href="%s"><b>%s</b></a> - %s',
3429 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 ),
3430 $deactivate_plugin_title,
3431 $deactivate_plugin_desc
3432 )
3433 )
3434 );
3435 break;
3436 // default:
3437 // $message = $this->get_text_inline( 'connectivity-test-fails-message' );
3438 // break;
3439 }
3440 }
3441
3442 $message_id = 'failed_connect_api';
3443 $type = 'error';
3444
3445 $connectivity_test_fails_message = $this->esc_html_inline( 'From unknown reason, the API connectivity test failed.', 'connectivity-test-fails-message' );
3446
3447 if ( false === $message ) {
3448 if ( $is_first_failure ) {
3449 // First attempt failed.
3450 $message = sprintf(
3451 $x_requires_access_to_api . ' ' .
3452 $connectivity_test_fails_message . ' ' .
3453 $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>' .
3454 '%s',
3455 '<b>' . $this->get_plugin_name() . '</b>',
3456 sprintf(
3457 '<div id="fs_firewall_issue_options">%s %s</div>',
3458 sprintf(
3459 '<a class="button button-primary fs-resolve" data-type="retry_ping" href="#">%s</a>',
3460 $this->get_text_inline( 'Yes - do your thing', 'yes-do-your-thing' )
3461 ),
3462 sprintf(
3463 '<a href="%s" class="button">%s</a>',
3464 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 ),
3465 $this->get_text_inline( 'No - just deactivate', 'no-deactivate' )
3466 )
3467 )
3468 );
3469
3470 $message_id = 'failed_connect_api_first';
3471 $type = 'promotion';
3472 } else {
3473 // Second connectivity attempt failed.
3474 $message = sprintf(
3475 $x_requires_access_to_api . ' ' .
3476 $connectivity_test_fails_message . ' ' .
3477 $happy_to_resolve_issue_asap .
3478 ' %s',
3479 '<b>' . $this->get_plugin_name() . '</b>',
3480 sprintf(
3481 '<ol id="fs_firewall_issue_options"><li>%s</li><li>%s</li><li>%s</li></ol>',
3482 sprintf(
3483 '<a class="fs-resolve" data-type="general" href="#"><b>%s</b></a>%s',
3484 $fix_issue_title,
3485 ' - ' . sprintf(
3486 $fix_issue_desc,
3487 '<a href="mailto:' . $admin_email . '">' . $admin_email . '</a>'
3488 )
3489 ),
3490 sprintf(
3491 '<a href="%s" target="_blank"><b>%s</b></a> - %s',
3492 sprintf( 'https://wordpress.org/plugins/%s/download/', $this->_slug ),
3493 $install_previous_title,
3494 $install_previous_desc
3495 ),
3496 sprintf(
3497 '<a href="%s"><b>%s</b></a> - %s',
3498 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 ),
3499 $deactivate_plugin_title,
3500 $deactivate_plugin_desc
3501 )
3502 )
3503 );
3504 }
3505 }
3506
3507 $this->_admin_notices->add_sticky(
3508 $message,
3509 $message_id,
3510 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
3511 $type
3512 );
3513 }
3514
3515 /**
3516 * Handle user request to resolve connectivity issue.
3517 * This method will send an email to Freemius API technical staff for resolution.
3518 * The email will contain server's info and installed plugins (might be caching issue).
3519 *
3520 * @author Vova Feldman (@svovaf)
3521 * @since 1.0.9
3522 */
3523 function _email_about_firewall_issue() {
3524 $this->_admin_notices->remove_sticky( 'failed_connect_api' );
3525
3526 $pong = $this->ping();
3527
3528 $is_connected = $this->get_api_plugin_scope()->is_valid_ping( $pong );
3529
3530 if ( $is_connected ) {
3531 $this->store_connectivity_info( $pong, $is_connected );
3532
3533 echo $this->get_after_plugin_activation_redirect_url();
3534 exit;
3535 }
3536
3537 $current_user = self::_get_current_wp_user();
3538 $admin_email = $current_user->user_email;
3539
3540 $error_type = fs_request_get( 'error_type', 'general' );
3541
3542 switch ( $error_type ) {
3543 case 'squid':
3544 $title = 'Squid ACL Blocking Issue';
3545 break;
3546 case 'cloudflare':
3547 $title = 'CloudFlare Blocking Issue';
3548 break;
3549 default:
3550 $title = 'API Connectivity Issue';
3551 break;
3552 }
3553
3554 $custom_email_sections = array();
3555
3556 // Add 'API Error' custom email section.
3557 $custom_email_sections['api_error'] = array(
3558 'title' => 'API Error',
3559 'rows' => array(
3560 'ping' => array(
3561 'API Error',
3562 is_string( $pong ) ? htmlentities( $pong ) : json_encode( $pong )
3563 ),
3564 )
3565 );
3566
3567 // Send email with technical details to resolve API connectivity issues.
3568 $this->send_email(
3569 'api@freemius.com', // recipient
3570 $title . ' [' . $this->get_plugin_name() . ']', // subject
3571 $custom_email_sections,
3572 array( "Reply-To: $admin_email <$admin_email>" ) // headers
3573 );
3574
3575 $this->_admin_notices->add_sticky(
3576 sprintf(
3577 $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' ),
3578 '<a href="mailto:' . $admin_email . '">' . $admin_email . '</a>'
3579 ),
3580 'server_details_sent'
3581 );
3582
3583 // Action was taken, tell that API connectivity troubleshooting should be off now.
3584
3585 echo "1";
3586 exit;
3587 }
3588
3589 /**
3590 * Handle connectivity test retry approved by the user.
3591 *
3592 * @author Vova Feldman (@svovaf)
3593 * @since 1.1.7.4
3594 */
3595 function _retry_connectivity_test() {
3596 $this->_admin_notices->remove_sticky( 'failed_connect_api_first' );
3597
3598 $pong = $this->ping();
3599
3600 $is_connected = $this->get_api_plugin_scope()->is_valid_ping( $pong );
3601
3602 if ( $is_connected ) {
3603 $this->store_connectivity_info( $pong, $is_connected );
3604
3605 echo $this->get_after_plugin_activation_redirect_url();
3606 } else {
3607 // Add connectivity issue message after 2nd failed attempt.
3608 $this->_add_connectivity_issue_message( $pong, false );
3609
3610 echo "1";
3611 }
3612
3613 exit;
3614 }
3615
3616 static function _add_firewall_issues_javascript() {
3617 $params = array();
3618 fs_require_once_template( 'firewall-issues-js.php', $params );
3619 }
3620
3621 #endregion
3622
3623 #----------------------------------------------------------------------------------
3624 #region Email
3625 #----------------------------------------------------------------------------------
3626
3627 /**
3628 * Generates and sends an HTML email with customizable sections.
3629 *
3630 * @author Leo Fajardo (@leorw)
3631 * @since 1.1.2
3632 *
3633 * @param string $to_address
3634 * @param string $subject
3635 * @param array $sections
3636 * @param array $headers
3637 *
3638 * @return bool Whether the email contents were sent successfully.
3639 */
3640 private function send_email(
3641 $to_address,
3642 $subject,
3643 $sections = array(),
3644 $headers = array()
3645 ) {
3646 $default_sections = $this->get_email_sections();
3647
3648 // Insert new sections or replace the default email sections.
3649 if ( is_array( $sections ) && ! empty( $sections ) ) {
3650 foreach ( $sections as $section_id => $custom_section ) {
3651 if ( ! isset( $default_sections[ $section_id ] ) ) {
3652 // If the section does not exist, add it.
3653 $default_sections[ $section_id ] = $custom_section;
3654 } else {
3655 // If the section already exists, override it.
3656 $current_section = $default_sections[ $section_id ];
3657
3658 // Replace the current section's title if a custom section title exists.
3659 if ( isset( $custom_section['title'] ) ) {
3660 $current_section['title'] = $custom_section['title'];
3661 }
3662
3663 // Insert new rows under the current section or replace the default rows.
3664 if ( isset( $custom_section['rows'] ) && is_array( $custom_section['rows'] ) && ! empty( $custom_section['rows'] ) ) {
3665 foreach ( $custom_section['rows'] as $row_id => $row ) {
3666 $current_section['rows'][ $row_id ] = $row;
3667 }
3668 }
3669
3670 $default_sections[ $section_id ] = $current_section;
3671 }
3672 }
3673 }
3674
3675 $vars = array( 'sections' => $default_sections );
3676 $message = fs_get_template( 'email.php', $vars );
3677
3678 // Set the type of email to HTML.
3679 $headers[] = 'Content-type: text/html; charset=UTF-8';
3680
3681 $header_string = implode( "\r\n", $headers );
3682
3683 return wp_mail(
3684 $to_address,
3685 $subject,
3686 $message,
3687 $header_string
3688 );
3689 }
3690
3691 /**
3692 * Generates the data for the sections of the email content.
3693 *
3694 * @author Leo Fajardo (@leorw)
3695 * @since 1.1.2
3696 *
3697 * @return array
3698 */
3699 private function get_email_sections() {
3700 // Retrieve the current user's information so that we can get the user's email, first name, and last name below.
3701 $current_user = self::_get_current_wp_user();
3702
3703 // Retrieve the cURL version information so that we can get the version number below.
3704 $curl_version_information = curl_version();
3705
3706 $active_plugin = self::get_active_plugins();
3707
3708 // Generate the list of active plugins separated by new line.
3709 $active_plugin_string = '';
3710 foreach ( $active_plugin as $plugin ) {
3711 $active_plugin_string .= sprintf(
3712 '<a href="%s">%s</a> [v%s]<br>',
3713 $plugin['PluginURI'],
3714 $plugin['Name'],
3715 $plugin['Version']
3716 );
3717 }
3718
3719 $server_ip = WP_FS__REMOTE_ADDR;
3720
3721 // Add PHP info for deeper investigation.
3722 ob_start();
3723 phpinfo();
3724 $php_info = ob_get_clean();
3725
3726 $api_domain = substr( FS_API__ADDRESS, strpos( FS_API__ADDRESS, ':' ) + 3 );
3727
3728 // Generate the default email sections.
3729 $sections = array(
3730 'sdk' => array(
3731 'title' => 'SDK',
3732 'rows' => array(
3733 'fs_version' => array( 'FS Version', $this->version ),
3734 'curl_version' => array( 'cURL Version', $curl_version_information['version'] )
3735 )
3736 ),
3737 'plugin' => array(
3738 'title' => ucfirst( $this->get_module_type() ),
3739 'rows' => array(
3740 'name' => array( 'Name', $this->get_plugin_name() ),
3741 'version' => array( 'Version', $this->get_plugin_version() )
3742 )
3743 ),
3744 'api' => array(
3745 'title' => 'API Subdomain',
3746 'rows' => array(
3747 'dns' => array(
3748 'DNS_CNAME',
3749 function_exists( 'dns_get_record' ) ?
3750 var_export( dns_get_record( $api_domain, DNS_CNAME ), true ) :
3751 'dns_get_record() disabled/blocked'
3752 ),
3753 'ip' => array(
3754 'IP',
3755 function_exists( 'gethostbyname' ) ?
3756 gethostbyname( $api_domain ) :
3757 'gethostbyname() disabled/blocked'
3758 ),
3759 ),
3760 ),
3761 'site' => array(
3762 'title' => 'Site',
3763 'rows' => array(
3764 'unique_id' => array( 'Unique ID', $this->get_anonymous_id() ),
3765 'address' => array( 'Address', site_url() ),
3766 'host' => array(
3767 'HTTP_HOST',
3768 ( ! empty( $_SERVER['HTTP_HOST'] ) ? $_SERVER['HTTP_HOST'] : '' )
3769 ),
3770 'hosting' => array(
3771 'Hosting Company' => fs_request_has( 'hosting_company' ) ?
3772 fs_request_get( 'hosting_company' ) :
3773 'Unknown',
3774 ),
3775 'server_addr' => array(
3776 'SERVER_ADDR',
3777 '<a href="http://www.projecthoneypot.org/ip_' . $server_ip . '">' . $server_ip . '</a>'
3778 )
3779 )
3780 ),
3781 'user' => array(
3782 'title' => 'User',
3783 'rows' => array(
3784 'email' => array( 'Email', $current_user->user_email ),
3785 'first' => array( 'First', $current_user->user_firstname ),
3786 'last' => array( 'Last', $current_user->user_lastname )
3787 )
3788 ),
3789 'plugins' => array(
3790 'title' => 'Plugins',
3791 'rows' => array(
3792 'active_plugins' => array( 'Active Plugins', $active_plugin_string )
3793 )
3794 ),
3795 'php_info' => array(
3796 'title' => 'PHP Info',
3797 'rows' => array(
3798 'info' => array( $php_info )
3799 ),
3800 )
3801 );
3802
3803 // Allow the sections to be modified by other code.
3804 $sections = $this->apply_filters( 'email_template_sections', $sections );
3805
3806 return $sections;
3807 }
3808
3809 #endregion
3810
3811 #----------------------------------------------------------------------------------
3812 #region Initialization
3813 #----------------------------------------------------------------------------------
3814
3815 /**
3816 * Init plugin's Freemius instance.
3817 *
3818 * @author Vova Feldman (@svovaf)
3819 * @since 1.0.1
3820 *
3821 * @param number $id
3822 * @param string $public_key
3823 * @param bool $is_live
3824 * @param bool $is_premium
3825 */
3826 function init( $id, $public_key, $is_live = true, $is_premium = true ) {
3827 $this->_logger->entrance();
3828
3829 $this->dynamic_init( array(
3830 'id' => $id,
3831 'public_key' => $public_key,
3832 'is_live' => $is_live,
3833 'is_premium' => $is_premium,
3834 ) );
3835 }
3836
3837 /**
3838 * Dynamic initiator, originally created to support initiation
3839 * with parent_id for add-ons.
3840 *
3841 * @author Vova Feldman (@svovaf)
3842 * @since 1.0.6
3843 *
3844 * @param array $plugin_info
3845 *
3846 * @throws Freemius_Exception
3847 */
3848 function dynamic_init( array $plugin_info ) {
3849 $this->_logger->entrance();
3850
3851 $this->parse_settings( $plugin_info );
3852
3853 if ( ! self::is_ajax() ) {
3854 if ( ! $this->is_addon() || $this->is_only_premium() ) {
3855 add_action(
3856 ( $this->_is_network_active && fs_is_network_admin() ? 'network_' : '' ) . 'admin_menu',
3857 array( &$this, '_prepare_admin_menu' ),
3858 WP_FS__LOWEST_PRIORITY
3859 );
3860 }
3861 }
3862
3863 if ( $this->should_stop_execution() ) {
3864 return;
3865 }
3866
3867 if ( ! $this->is_registered() ) {
3868 if ( $this->is_anonymous() ) {
3869 // If user skipped, no need to test connectivity.
3870 $this->_has_api_connection = true;
3871 $this->_is_on = true;
3872 } else {
3873 if ( ! $this->has_api_connectivity() ) {
3874 if ( $this->_admin_notices->has_sticky( 'failed_connect_api_first' ) ||
3875 $this->_admin_notices->has_sticky( 'failed_connect_api' )
3876 ) {
3877 if ( ! $this->_enable_anonymous || $this->is_premium() ) {
3878 // If anonymous mode is disabled, add firewall admin-notice message.
3879 add_action( 'admin_footer', array( 'Freemius', '_add_firewall_issues_javascript' ) );
3880
3881 $ajax_action_suffix = $this->_slug . ( $this->is_theme() ? ':theme' : '' );
3882 add_action( "wp_ajax_fs_resolve_firewall_issues_{$ajax_action_suffix}", array(
3883 &$this,
3884 '_email_about_firewall_issue'
3885 ) );
3886
3887 add_action( "wp_ajax_fs_retry_connectivity_test_{$ajax_action_suffix}", array(
3888 &$this,
3889 '_retry_connectivity_test'
3890 ) );
3891
3892 /**
3893 * 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.
3894 *
3895 * @author Vova Feldman (@svovaf)
3896 * @since 2.0.0
3897 */
3898 /*$this->add_ajax_action( 'resolve_firewall_issues', array(
3899 &$this,
3900 '_email_about_firewall_issue'
3901 ) );
3902
3903 $this->add_ajax_action( 'retry_connectivity_test', array(
3904 &$this,
3905 '_retry_connectivity_test'
3906 ) );*/
3907 }
3908 }
3909
3910 return;
3911 } else {
3912 $this->_admin_notices->remove_sticky( array(
3913 'failed_connect_api_first',
3914 'failed_connect_api',
3915 ) );
3916
3917 if ( $this->_anonymous_mode ) {
3918 // Simulate anonymous mode.
3919 $this->_is_anonymous = true;
3920 }
3921 }
3922 }
3923 }
3924
3925 /**
3926 * This should be executed even if Freemius is off for the core module,
3927 * otherwise, the add-ons dialogbox won't work properly. This is esepcially
3928 * relevant when the developer decided to turn FS off for existing users.
3929 *
3930 * @author Vova Feldman (@svovaf)
3931 */
3932 if ( $this->is_user_in_admin() &&
3933 ! $this->is_addon() &&
3934 $this->has_addons() &&
3935 'plugin-information' === fs_request_get( 'tab', false ) &&
3936 $this->get_id() == fs_request_get( 'parent_plugin_id', false )
3937 ) {
3938 require_once WP_FS__DIR_INCLUDES . '/fs-plugin-info-dialog.php';
3939
3940 new FS_Plugin_Info_Dialog( $this );
3941 }
3942
3943 // Check if Freemius is on for the current plugin.
3944 // This MUST be executed after all the plugin variables has been loaded.
3945 if ( ! $this->is_registered() && ! $this->is_on() ) {
3946 return;
3947 }
3948
3949 if ( $this->has_api_connectivity() ) {
3950 if ( self::is_cron() ) {
3951 $this->hook_callback_to_sync_cron();
3952 } else if ( $this->is_user_in_admin() ) {
3953 /**
3954 * Schedule daily data sync cron if:
3955 *
3956 * 1. User opted-in (for tracking).
3957 * 2. If skipped, but later upgraded (opted-in via upgrade).
3958 *
3959 * @author Vova Feldman (@svovaf)
3960 * @since 1.1.7.3
3961 *
3962 */
3963 if ( $this->is_registered() ) {
3964 if ( ! $this->is_sync_cron_on() && $this->is_tracking_allowed() ) {
3965 $this->schedule_sync_cron();
3966 }
3967 }
3968
3969 /**
3970 * Check if requested for manual blocking background sync.
3971 */
3972 if ( fs_request_has( 'background_sync' ) ) {
3973 $this->run_manual_sync();
3974 }
3975 }
3976 }
3977
3978 if ( $this->is_registered() ) {
3979 $this->hook_callback_to_install_sync();
3980 }
3981
3982 if ( $this->is_addon() ) {
3983 if ( $this->is_parent_plugin_installed() ) {
3984 // Link to parent FS.
3985 $this->_parent = self::get_instance_by_id( $this->_plugin->parent_plugin_id );
3986
3987 // Get parent plugin reference.
3988 $this->_parent_plugin = $this->_parent->get_plugin();
3989 }
3990 }
3991
3992 if ( $this->is_user_in_admin() ) {
3993 if ( $this->is_addon() ) {
3994 if ( ! $this->is_parent_plugin_installed() ) {
3995 $parent_name = $this->get_option( $plugin_info, 'parent_name', null );
3996
3997 if ( isset( $plugin_info['parent'] ) ) {
3998 $parent_name = $this->get_option( $plugin_info['parent'], 'name', null );
3999 }
4000
4001 $this->_admin_notices->add(
4002 ( ! empty( $parent_name ) ?
4003 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 ) :
4004 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() )
4005 ),
4006 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
4007 'error'
4008 );
4009
4010 return;
4011 } else {
4012 if ( $this->_parent->is_registered() && ! $this->is_registered() ) {
4013 // If parent plugin activated, automatically install add-on for the user.
4014 $this->_activate_addon_account( $this->_parent );
4015 } else if ( ! $this->_parent->is_registered() && $this->is_registered() ) {
4016 // If add-on activated and parent not, automatically install parent for the user.
4017 $this->activate_parent_account( $this->_parent );
4018 }
4019
4020 // @todo This should be only executed on activation. It should be migrated to register_activation_hook() together with other activation related logic.
4021 if ( $this->is_premium() ) {
4022 // Remove add-on download admin-notice.
4023 $this->_parent->_admin_notices->remove_sticky( array(
4024 'addon_plan_upgraded_' . $this->_slug,
4025 'no_addon_license_' . $this->_slug,
4026 ) );
4027 }
4028
4029 // $this->deactivate_premium_only_addon_without_license();
4030 }
4031 }
4032
4033 add_action( 'admin_init', array( &$this, '_admin_init_action' ) );
4034
4035 // if ( $this->is_registered() ||
4036 // $this->is_anonymous() ||
4037 // $this->is_pending_activation()
4038 // ) {
4039 // $this->_init_admin();
4040 // }
4041 }
4042
4043 /**
4044 * Should be called outside `$this->is_user_in_admin()` scope
4045 * because the updater has some logic that needs to be executed
4046 * during AJAX calls.
4047 *
4048 * Currently we need to hook to the `http_request_host_is_external` filter.
4049 * In the future, there might be additional logic added.
4050 *
4051 * @author Vova Feldman
4052 * @since 1.2.1.6
4053 */
4054 if ( $this->is_premium() && $this->has_release_on_freemius() ) {
4055 FS_Plugin_Updater::instance( $this );
4056 }
4057
4058 $this->do_action( 'initiated' );
4059
4060 if ( $this->_storage->prev_is_premium !== $this->_plugin->is_premium ) {
4061 if ( isset( $this->_storage->prev_is_premium ) ) {
4062 $this->apply_filters(
4063 'after_code_type_change',
4064 // New code type.
4065 $this->_plugin->is_premium
4066 );
4067 } else {
4068 // Set for code type for the first time.
4069 $this->_storage->prev_is_premium = $this->_plugin->is_premium;
4070 }
4071 }
4072
4073 if ( ! $this->is_addon() ) {
4074 if ( $this->is_registered() ) {
4075 // Fix for upgrade from versions < 1.0.9.
4076 if ( ! isset( $this->_storage->activation_timestamp ) ) {
4077 $this->_storage->activation_timestamp = WP_FS__SCRIPT_START_TIME;
4078 }
4079
4080 $this->do_action( 'after_init_plugin_registered' );
4081 } else if ( $this->is_anonymous() ) {
4082 $this->do_action( 'after_init_plugin_anonymous' );
4083 } else if ( $this->is_pending_activation() ) {
4084 $this->do_action( 'after_init_plugin_pending_activations' );
4085 }
4086 } else {
4087 if ( $this->is_registered() ) {
4088 $this->do_action( 'after_init_addon_registered' );
4089 } else if ( $this->is_anonymous() ) {
4090 $this->do_action( 'after_init_addon_anonymous' );
4091 } else if ( $this->is_pending_activation() ) {
4092 $this->do_action( 'after_init_addon_pending_activations' );
4093 }
4094 }
4095 }
4096
4097 /**
4098 * @author Leo Fajardo (@leorw)
4099 *
4100 * @since 1.2.1.5
4101 */
4102 function _stop_tracking_callback() {
4103 $this->_logger->entrance();
4104
4105 $this->check_ajax_referer( 'stop_tracking' );
4106
4107 $result = $this->stop_tracking( fs_is_network_admin() );
4108
4109 if ( true === $result ) {
4110 self::shoot_ajax_success();
4111 }
4112
4113 $this->_logger->api_error( $result );
4114
4115 self::shoot_ajax_failure(
4116 sprintf( $this->get_text_inline( 'Unexpected API error. Please contact the %s\'s author with the following error.', 'unexpected-api-error' ), $this->_module_type ) .
4117 ( $this->is_api_error( $result ) && isset( $result->error ) ?
4118 $result->error->message :
4119 var_export( $result, true ) )
4120 );
4121 }
4122
4123 /**
4124 * @author Leo Fajardo (@leorw)
4125 * @since 1.2.1.5
4126 */
4127 function _allow_tracking_callback() {
4128 $this->_logger->entrance();
4129
4130 $this->check_ajax_referer( 'allow_tracking' );
4131
4132 $result = $this->allow_tracking( fs_is_network_admin() );
4133
4134 if ( true === $result ) {
4135 self::shoot_ajax_success();
4136 }
4137
4138 $this->_logger->api_error( $result );
4139
4140 self::shoot_ajax_failure(
4141 sprintf( $this->get_text_inline( 'Unexpected API error. Please contact the %s\'s author with the following error.', 'unexpected-api-error' ), $this->_module_type ) .
4142 ( $this->is_api_error( $result ) && isset( $result->error ) ?
4143 $result->error->message :
4144 var_export( $result, true ) )
4145 );
4146 }
4147
4148 /**
4149 * Opt-out from usage tracking.
4150 *
4151 * Note: This will not delete the account information but will stop all tracking.
4152 *
4153 * Returns:
4154 * 1. FALSE - If the user never opted-in.
4155 * 2. TRUE - If successfully opted-out.
4156 * 3. object - API result on failure.
4157 *
4158 * @author Leo Fajardo (@leorw)
4159 * @since 1.2.1.5
4160 *
4161 * @return bool|object
4162 */
4163 function stop_site_tracking() {
4164 $this->_logger->entrance();
4165
4166 if ( ! $this->is_registered() ) {
4167 // User never opted-in.
4168 return false;
4169 }
4170
4171 if ( $this->is_tracking_prohibited() ) {
4172 // Already disconnected.
4173 return true;
4174 }
4175
4176 // Send update to FS.
4177 $result = $this->get_api_site_scope()->call( '/?fields=is_disconnected', 'put', array(
4178 'is_disconnected' => true
4179 ) );
4180
4181 if ( ! $this->is_api_result_entity( $result ) ||
4182 ! isset( $result->is_disconnected ) ||
4183 ! $result->is_disconnected
4184 ) {
4185 $this->_logger->api_error( $result );
4186
4187 return $result;
4188 }
4189
4190 $this->_site->is_disconnected = $result->is_disconnected;
4191 $this->_store_site();
4192
4193 $this->clear_sync_cron();
4194
4195 // Successfully disconnected.
4196 return true;
4197 }
4198
4199 /**
4200 * Opt-out network from usage tracking.
4201 *
4202 * Note: This will not delete the account information but will stop all tracking.
4203 *
4204 * Returns:
4205 * 1. FALSE - If the user never opted-in.
4206 * 2. TRUE - If successfully opted-out.
4207 * 3. object - API result on failure.
4208 *
4209 * @author Leo Fajardo (@leorw)
4210 * @since 1.2.1.5
4211 *
4212 * @return bool|object
4213 */
4214 function stop_network_tracking() {
4215 $this->_logger->entrance();
4216
4217 if ( ! $this->is_registered() ) {
4218 // User never opted-in.
4219 return false;
4220 }
4221
4222 $install_id_2_blog_id = array();
4223 $installs_map = $this->get_blog_install_map();
4224
4225 $opt_out_all = true;
4226
4227 $params = array();
4228 foreach ( $installs_map as $blog_id => $install ) {
4229 if ( $install->is_tracking_prohibited() ) {
4230 // Already opted-out.
4231 continue;
4232 }
4233
4234 if ( $this->is_site_delegated_connection( $blog_id ) ) {
4235 // Opt-out only from non-delegated installs.
4236 $opt_out_all = false;
4237 continue;
4238 }
4239
4240 $params[] = array( 'id' => $install->id );
4241
4242 $install_id_2_blog_id[ $install->id ] = $blog_id;
4243 }
4244
4245 if ( empty( $install_id_2_blog_id ) ) {
4246 return true;
4247 }
4248
4249 $params[] = array( 'is_disconnected' => true );
4250
4251 // Send update to FS.
4252 $result = $this->get_current_or_network_user_api_scope()->call( "/plugins/{$this->_module_id}/installs.json", 'put', $params );
4253
4254 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
4255 $this->_logger->api_error( $result );
4256
4257 return $result;
4258 }
4259
4260 foreach ( $result->installs as $r_install ) {
4261 $blog_id = $install_id_2_blog_id[ $r_install->id ];
4262 $install = $installs_map[ $blog_id ];
4263 $install->is_disconnected = $r_install->is_disconnected;
4264 $this->_store_site( true, $blog_id, $install );
4265 }
4266
4267 $this->clear_sync_cron( $opt_out_all );
4268
4269 // Successfully disconnected.
4270 return true;
4271 }
4272
4273 /**
4274 * Opt-out from usage tracking.
4275 *
4276 * Note: This will not delete the account information but will stop all tracking.
4277 *
4278 * Returns:
4279 * 1. FALSE - If the user never opted-in.
4280 * 2. TRUE - If successfully opted-out.
4281 * 3. object - API result on failure.
4282 *
4283 * @author Leo Fajardo (@leorw)
4284 * @since 1.2.1.5
4285 *
4286 * @param bool $is_network_action
4287 *
4288 * @return bool|object
4289 */
4290 function stop_tracking( $is_network_action = false ) {
4291 $this->_logger->entrance();
4292
4293 return $is_network_action ?
4294 $this->stop_network_tracking() :
4295 $this->stop_site_tracking();
4296 }
4297
4298 /**
4299 * Opt-in back into usage tracking.
4300 *
4301 * Note: This will only work if the user opted-in previously.
4302 *
4303 * Returns:
4304 * 1. FALSE - If the user never opted-in.
4305 * 2. TRUE - If successfully opted-in back to usage tracking.
4306 * 3. object - API result on failure.
4307 *
4308 * @author Leo Fajardo (@leorw)
4309 * @since 1.2.1.5
4310 *
4311 * @return bool|object
4312 */
4313 function allow_site_tracking() {
4314 $this->_logger->entrance();
4315
4316 if ( ! $this->is_registered() ) {
4317 // User never opted-in.
4318 return false;
4319 }
4320
4321 if ( $this->is_tracking_allowed() ) {
4322 // Tracking already allowed.
4323 return true;
4324 }
4325
4326 $result = $this->get_api_site_scope()->call( '/?is_disconnected', 'put', array(
4327 'is_disconnected' => false
4328 ) );
4329
4330 if ( ! $this->is_api_result_entity( $result ) ||
4331 ! isset( $result->is_disconnected ) ||
4332 $result->is_disconnected
4333 ) {
4334 $this->_logger->api_error( $result );
4335
4336 return $result;
4337 }
4338
4339 $this->_site->is_disconnected = $result->is_disconnected;
4340 $this->_store_site();
4341
4342 $this->schedule_sync_cron();
4343
4344 // Successfully reconnected.
4345 return true;
4346 }
4347
4348 /**
4349 * Opt-in network back into usage tracking.
4350 *
4351 * Note: This will only work if the user opted-in previously.
4352 *
4353 * Returns:
4354 * 1. FALSE - If the user never opted-in.
4355 * 2. TRUE - If successfully opted-in back to usage tracking.
4356 * 3. object - API result on failure.
4357 *
4358 * @author Leo Fajardo (@leorw)
4359 * @since 1.2.1.5
4360 *
4361 * @return bool|object
4362 */
4363 function allow_network_tracking() {
4364 $this->_logger->entrance();
4365
4366 if ( ! $this->is_registered() ) {
4367 // User never opted-in.
4368 return false;
4369 }
4370
4371 $install_id_2_blog_id = array();
4372 $installs_map = $this->get_blog_install_map();
4373
4374 $params = array();
4375 foreach ( $installs_map as $blog_id => $install ) {
4376 if ( $install->is_tracking_allowed() ) {
4377 continue;
4378 }
4379
4380 $params[] = array( 'id' => $install->id );
4381
4382 $install_id_2_blog_id[ $install->id ] = $blog_id;
4383 }
4384
4385 if ( empty( $install_id_2_blog_id ) ) {
4386 return true;
4387 }
4388
4389 $params[] = array( 'is_disconnected' => false );
4390
4391 // Send update to FS.
4392 $result = $this->get_current_or_network_user_api_scope()->call( "/plugins/{$this->_module_id}/installs.json", 'put', $params );
4393
4394
4395 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
4396 $this->_logger->api_error( $result );
4397
4398 return $result;
4399 }
4400
4401 foreach ( $result->installs as $r_install ) {
4402 $blog_id = $install_id_2_blog_id[ $r_install->id ];
4403 $install = $installs_map[ $blog_id ];
4404 $install->is_disconnected = $r_install->is_disconnected;
4405 $this->_store_site( true, $blog_id, $install );
4406 }
4407
4408 $this->schedule_sync_cron();
4409
4410 // Successfully reconnected.
4411 return true;
4412 }
4413
4414 /**
4415 * Opt-in back into usage tracking.
4416 *
4417 * Note: This will only work if the user opted-in previously.
4418 *
4419 * Returns:
4420 * 1. FALSE - If the user never opted-in.
4421 * 2. TRUE - If successfully opted-in back to usage tracking.
4422 * 3. object - API result on failure.
4423 *
4424 * @author Leo Fajardo (@leorw)
4425 * @since 1.2.1.5
4426 *
4427 * @param bool $is_network_action
4428 *
4429 * @return bool|object
4430 */
4431 function allow_tracking( $is_network_action = false ) {
4432 $this->_logger->entrance();
4433
4434 return $is_network_action ?
4435 $this->allow_network_tracking() :
4436 $this->allow_site_tracking();
4437 }
4438
4439 /**
4440 * If user opted-in and later disabled usage-tracking,
4441 * re-allow tracking for licensing and updates.
4442 *
4443 * @author Leo Fajardo (@leorw)
4444 * @since 1.2.1.5
4445 *
4446 * @param bool $is_context_single_site
4447 */
4448 private function reconnect_locally( $is_context_single_site = false ) {
4449 $this->_logger->entrance();
4450
4451 if ( ! $this->is_registered() ) {
4452 return;
4453 }
4454
4455 if ( ! fs_is_network_admin() || $is_context_single_site ) {
4456 if ( $this->is_tracking_prohibited() ) {
4457 $this->_site->is_disconnected = false;
4458 $this->_store_site();
4459 }
4460 } else {
4461 $installs_map = $this->get_blog_install_map();
4462 foreach ( $installs_map as $blog_id => $install ) {
4463 /**
4464 * @var FS_Site $install
4465 */
4466 if ( $install->is_tracking_prohibited() ) {
4467 $install->is_disconnected = false;
4468 $this->_store_site( true, $blog_id, $install );
4469 }
4470 }
4471 }
4472 }
4473
4474 /**
4475 * Parse plugin's settings (as defined by the plugin dev).
4476 *
4477 * @author Vova Feldman (@svovaf)
4478 * @since 1.1.7.3
4479 *
4480 * @param array $plugin_info
4481 *
4482 * @throws \Freemius_Exception
4483 */
4484 private function parse_settings( &$plugin_info ) {
4485 $this->_logger->entrance();
4486
4487 $id = $this->get_numeric_option( $plugin_info, 'id', false );
4488 $public_key = $this->get_option( $plugin_info, 'public_key', false );
4489 $secret_key = $this->get_option( $plugin_info, 'secret_key', null );
4490 $parent_id = $this->get_numeric_option( $plugin_info, 'parent_id', null );
4491 $parent_name = $this->get_option( $plugin_info, 'parent_name', null );
4492
4493 /**
4494 * @author Vova Feldman (@svovaf)
4495 * @since 1.1.9 Try to pull secret key from external config.
4496 */
4497 if ( is_null( $secret_key ) && defined( "WP_FS__{$this->_slug}_SECRET_KEY" ) ) {
4498 $secret_key = constant( "WP_FS__{$this->_slug}_SECRET_KEY" );
4499 }
4500
4501 if ( isset( $plugin_info['parent'] ) ) {
4502 $parent_id = $this->get_numeric_option( $plugin_info['parent'], 'id', null );
4503 // $parent_slug = $this->get_option( $plugin_info['parent'], 'slug', null );
4504 // $parent_public_key = $this->get_option( $plugin_info['parent'], 'public_key', null );
4505 // $parent_name = $this->get_option( $plugin_info['parent'], 'name', null );
4506 }
4507
4508 if ( false === $id ) {
4509 throw new Freemius_Exception( array(
4510 'error' => array(
4511 'type' => 'ParameterNotSet',
4512 'message' => 'Plugin id parameter is not set.',
4513 'code' => 'plugin_id_not_set',
4514 'http' => 500,
4515 )
4516 ) );
4517 }
4518 if ( false === $public_key ) {
4519 throw new Freemius_Exception( array(
4520 'error' => array(
4521 'type' => 'ParameterNotSet',
4522 'message' => 'Plugin public_key parameter is not set.',
4523 'code' => 'plugin_public_key_not_set',
4524 'http' => 500,
4525 )
4526 ) );
4527 }
4528
4529 $plugin = ( $this->_plugin instanceof FS_Plugin ) ?
4530 $this->_plugin :
4531 new FS_Plugin();
4532
4533 $plugin->update( array(
4534 'id' => $id,
4535 'type' => $this->get_option( $plugin_info, 'type', $this->_module_type ),
4536 'public_key' => $public_key,
4537 'slug' => $this->_slug,
4538 'parent_plugin_id' => $parent_id,
4539 'version' => $this->get_plugin_version(),
4540 'title' => $this->get_plugin_name(),
4541 'file' => $this->_plugin_basename,
4542 'is_premium' => $this->get_bool_option( $plugin_info, 'is_premium', true ),
4543 'is_live' => $this->get_bool_option( $plugin_info, 'is_live', true ),
4544 'affiliate_moderation' => $this->get_option( $plugin_info, 'has_affiliation' ),
4545 ) );
4546
4547 if ( $plugin->is_updated() ) {
4548 // Update plugin details.
4549 $this->_plugin = FS_Plugin_Manager::instance( $this->_module_id )->store( $plugin );
4550 }
4551 // Set the secret key after storing the plugin, we don't want to store the key in the storage.
4552 $this->_plugin->secret_key = $secret_key;
4553
4554 if ( ! isset( $plugin_info['menu'] ) ) {
4555 $plugin_info['menu'] = array();
4556
4557 if ( ! empty( $this->_storage->sdk_last_version ) &&
4558 version_compare( $this->_storage->sdk_last_version, '1.1.2', '<=' )
4559 ) {
4560 // Backward compatibility to 1.1.2
4561 $plugin_info['menu']['slug'] = isset( $plugin_info['menu_slug'] ) ?
4562 $plugin_info['menu_slug'] :
4563 $this->_slug;
4564 }
4565 }
4566
4567 $this->_menu = FS_Admin_Menu_Manager::instance(
4568 $this->_module_id,
4569 $this->_module_type,
4570 $this->get_unique_affix()
4571 );
4572
4573 $this->_menu->init( $plugin_info['menu'], $this->is_addon() );
4574
4575 $this->_has_addons = $this->get_bool_option( $plugin_info, 'has_addons', false );
4576 $this->_has_paid_plans = $this->get_bool_option( $plugin_info, 'has_paid_plans', true );
4577 $this->_has_premium_version = $this->get_bool_option( $plugin_info, 'has_premium_version', $this->_has_paid_plans );
4578 $this->_ignore_pending_mode = $this->get_bool_option( $plugin_info, 'ignore_pending_mode', false );
4579 $this->_is_org_compliant = $this->get_bool_option( $plugin_info, 'is_org_compliant', true );
4580 $this->_is_premium_only = $this->get_bool_option( $plugin_info, 'is_premium_only', false );
4581 if ( $this->_is_premium_only ) {
4582 // If premium only plugin, disable anonymous mode.
4583 $this->_enable_anonymous = false;
4584 $this->_anonymous_mode = false;
4585 } else {
4586 $this->_enable_anonymous = $this->get_bool_option( $plugin_info, 'enable_anonymous', true );
4587 $this->_anonymous_mode = $this->get_bool_option( $plugin_info, 'anonymous_mode', false );
4588 }
4589 $this->_permissions = $this->get_option( $plugin_info, 'permissions', array() );
4590
4591 if ( ! empty( $plugin_info['trial'] ) ) {
4592 $this->_trial_days = $this->get_numeric_option(
4593 $plugin_info['trial'],
4594 'days',
4595 // Default to 0 - trial without days specification.
4596 0
4597 );
4598
4599 $this->_is_trial_require_payment = $this->get_bool_option( $plugin_info['trial'], 'is_require_payment', false );
4600 }
4601 }
4602
4603 /**
4604 * @param string[] $options
4605 * @param string $key
4606 * @param mixed $default
4607 *
4608 * @return bool
4609 */
4610 private function get_option( &$options, $key, $default = false ) {
4611 return ! empty( $options[ $key ] ) ? $options[ $key ] : $default;
4612 }
4613
4614 private function get_bool_option( &$options, $key, $default = false ) {
4615 return isset( $options[ $key ] ) && is_bool( $options[ $key ] ) ? $options[ $key ] : $default;
4616 }
4617
4618 private function get_numeric_option( &$options, $key, $default = false ) {
4619 return isset( $options[ $key ] ) && is_numeric( $options[ $key ] ) ? $options[ $key ] : $default;
4620 }
4621
4622 /**
4623 * Gate keeper.
4624 *
4625 * @author Vova Feldman (@svovaf)
4626 * @since 1.1.7.3
4627 *
4628 * @return bool
4629 */
4630 private function should_stop_execution() {
4631 if ( empty( $this->_storage->was_plugin_loaded ) ) {
4632 /**
4633 * Don't execute Freemius until plugin was fully loaded at least once,
4634 * to give the opportunity for the activation hook to run before pinging
4635 * the API for connectivity test. This logic is relevant for the
4636 * identification of new plugin install vs. plugin update.
4637 *
4638 * @author Vova Feldman (@svovaf)
4639 * @since 1.1.9
4640 */
4641 return true;
4642 }
4643
4644 if ( $this->is_activation_mode() ) {
4645 if ( ! is_admin() ) {
4646 /**
4647 * If in activation mode, don't execute Freemius outside of the
4648 * admin dashboard.
4649 *
4650 * @author Vova Feldman (@svovaf)
4651 * @since 1.1.7.3
4652 */
4653 return true;
4654 }
4655
4656 if ( ! WP_FS__IS_HTTP_REQUEST ) {
4657 /**
4658 * If in activation and executed without HTTP context (e.g. CLI, Cronjob),
4659 * then don't start Freemius.
4660 *
4661 * @author Vova Feldman (@svovaf)
4662 * @since 1.1.6.3
4663 *
4664 * @link https://wordpress.org/support/topic/errors-in-the-freemius-class-when-running-in-wordpress-in-cli
4665 */
4666 return true;
4667 }
4668
4669 if ( self::is_cron() ) {
4670 /**
4671 * If in activation mode, don't execute Freemius during wp crons
4672 * (wp crons have HTTP context - called as HTTP request).
4673 *
4674 * @author Vova Feldman (@svovaf)
4675 * @since 1.1.7.3
4676 */
4677 return true;
4678 }
4679
4680 if ( self::is_ajax() &&
4681 ! $this->_admin_notices->has_sticky( 'failed_connect_api_first' ) &&
4682 ! $this->_admin_notices->has_sticky( 'failed_connect_api' )
4683 ) {
4684 /**
4685 * During activation, if running in AJAX mode, unless there's a sticky
4686 * connectivity issue notice, don't run Freemius.
4687 *
4688 * @author Vova Feldman (@svovaf)
4689 * @since 1.1.7.3
4690 */
4691 return true;
4692 }
4693 }
4694
4695 return false;
4696 }
4697
4698 /**
4699 * Triggered after code type has changed.
4700 *
4701 * @author Vova Feldman (@svovaf)
4702 * @since 1.1.9.1
4703 */
4704 function _after_code_type_change() {
4705 $this->_logger->entrance();
4706
4707 if ( $this->is_theme() ) {
4708 // Expire the cache of the previous tabs since the theme may
4709 // have setting updates after code type has changed.
4710 $this->_cache->expire( 'tabs' );
4711 $this->_cache->expire( 'tabs_stylesheets' );
4712 }
4713
4714 if ( $this->is_registered() ) {
4715 if ( ! $this->is_addon() ) {
4716 add_action(
4717 is_admin() ? 'admin_init' : 'init',
4718 array( &$this, '_plugin_code_type_changed' )
4719 );
4720 }
4721
4722 if ( $this->is_premium() ) {
4723 // Purge cached payments after switching to the premium version.
4724 // @todo This logic doesn't handle purging the cache for serviceware module upgrade.
4725 $this->get_api_user_scope()->purge_cache( "/plugins/{$this->_module_id}/payments.json?include_addons=true" );
4726 }
4727 }
4728 }
4729
4730 /**
4731 * Handles plugin's code type change (free <--> premium).
4732 *
4733 * @author Vova Feldman (@svovaf)
4734 * @since 1.0.9
4735 */
4736 function _plugin_code_type_changed() {
4737 $this->_logger->entrance();
4738
4739 if ( $this->is_premium() ) {
4740 $this->reconnect_locally();
4741
4742 // Activated premium code.
4743 $this->do_action( 'after_premium_version_activation' );
4744
4745 // Remove all sticky messages related to download of the premium version.
4746 $this->_admin_notices->remove_sticky( array(
4747 'trial_started',
4748 'plan_upgraded',
4749 'plan_changed',
4750 'license_activated',
4751 ) );
4752
4753 $notice = '';
4754 if ( ! $this->is_only_premium() ) {
4755 $notice = sprintf( $this->get_text_inline( 'Premium %s version was successfully activated.', 'premium-activated-message' ), $this->_module_type );
4756 }
4757
4758 $license_notice = $this->get_license_network_activation_notice();
4759 if ( ! empty( $license_notice ) ) {
4760 $notice .= ' ' . $license_notice;
4761 }
4762
4763 if ( ! empty( $notice ) ) {
4764 $this->_admin_notices->add_sticky(
4765 trim( $notice ),
4766 'premium_activated',
4767 $this->get_text_x_inline( 'W00t',
4768 'Used to express elation, enthusiasm, or triumph (especially in electronic communication).', 'woot' ) . '!'
4769 );
4770 }
4771 } else {
4772 // Remove sticky message related to premium code activation.
4773 $this->_admin_notices->remove_sticky( 'premium_activated' );
4774
4775 // Activated free code (after had the premium before).
4776 $this->do_action( 'after_free_version_reactivation' );
4777
4778 if ( $this->is_paying() && ! $this->is_premium() ) {
4779 $this->_admin_notices->add_sticky(
4780 sprintf(
4781 /* translators: %s: License type (e.g. you have a professional license) */
4782 $this->get_text_inline( 'You have a %s license.', 'you-have-x-license' ),
4783 $this->get_plan_title()
4784 ) . $this->get_complete_upgrade_instructions(),
4785 'plan_upgraded',
4786 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
4787 );
4788 }
4789 }
4790
4791 // Schedule code type changes event.
4792 $this->schedule_install_sync();
4793
4794 /**
4795 * Unregister the uninstall hook for the other version of the plugin (with different code type) to avoid
4796 * triggering a fatal error when uninstalling that plugin. For example, after deactivating the "free" version
4797 * of a specific plugin, its uninstall hook should be unregistered after the "premium" version has been
4798 * activated. If we don't do that, a fatal error will occur when we try to uninstall the "free" version since
4799 * the main file of the "free" version will be loaded first before calling the hooked callback. Since the
4800 * free and premium versions are almost identical (same class or have same functions), a fatal error like
4801 * "Cannot redeclare class MyClass" or "Cannot redeclare my_function()" will occur.
4802 */
4803 $this->unregister_uninstall_hook();
4804
4805 $this->clear_module_main_file_cache();
4806
4807 // Update is_premium of latest version.
4808 $this->_storage->prev_is_premium = $this->_plugin->is_premium;
4809 }
4810
4811 #endregion
4812
4813 #----------------------------------------------------------------------------------
4814 #region Add-ons
4815 #----------------------------------------------------------------------------------
4816
4817 /**
4818 * Check if add-on installed and activated on site.
4819 *
4820 * @author Vova Feldman (@svovaf)
4821 * @since 1.0.6
4822 *
4823 * @param string|number $id_or_slug
4824 * @param bool|null $is_premium Since 1.2.1.7 can check for specified add-on version.
4825 *
4826 * @return bool
4827 */
4828 function is_addon_activated( $id_or_slug, $is_premium = null ) {
4829 $this->_logger->entrance();
4830
4831 $addon_id = self::get_module_id( $id_or_slug );
4832 $is_activated = self::has_instance( $addon_id );
4833
4834 if ( ! $is_activated ) {
4835 return false;
4836 }
4837
4838 if ( is_bool( $is_premium ) ) {
4839 // Check if the specified code version is activate.
4840 $addon = $this->get_addon_instance( $addon_id );
4841 $is_activated = ( $is_premium === $addon->is_premium() );
4842 }
4843
4844 return $is_activated;
4845 }
4846
4847 /**
4848 * Check if add-on was connected to install
4849 *
4850 * @author Vova Feldman (@svovaf)
4851 * @since 1.1.7
4852 *
4853 * @param string|number $id_or_slug
4854 *
4855 * @return bool
4856 */
4857 function is_addon_connected( $id_or_slug ) {
4858 $this->_logger->entrance();
4859
4860 $sites = self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN );
4861
4862 $addon_id = self::get_module_id( $id_or_slug );
4863 $addon = $this->get_addon( $addon_id );
4864 $slug = $addon->slug;
4865 if ( ! isset( $sites[ $slug ] ) ) {
4866 return false;
4867 }
4868
4869 $site = $sites[ $slug ];
4870
4871 $plugin = FS_Plugin_Manager::instance( $addon_id )->get();
4872
4873 if ( $plugin->parent_plugin_id != $this->_plugin->id ) {
4874 // The given slug do NOT belong to any of the plugin's add-ons.
4875 return false;
4876 }
4877
4878 return ( is_object( $site ) &&
4879 is_numeric( $site->id ) &&
4880 is_numeric( $site->user_id ) &&
4881 FS_Plugin_Plan::is_valid_id( $site->plan_id )
4882 );
4883 }
4884
4885 /**
4886 * Determines if add-on installed.
4887 *
4888 * NOTE: This is a heuristic and only works if the folder/file named as the slug.
4889 *
4890 * @author Vova Feldman (@svovaf)
4891 * @since 1.0.6
4892 *
4893 * @param string|number $id_or_slug
4894 *
4895 * @return bool
4896 */
4897 function is_addon_installed( $id_or_slug ) {
4898 $this->_logger->entrance();
4899
4900 $addon_id = self::get_module_id( $id_or_slug );
4901
4902 return file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $this->get_addon_basename( $addon_id ) ) );
4903 }
4904
4905 /**
4906 * Get add-on basename.
4907 *
4908 * @author Vova Feldman (@svovaf)
4909 * @since 1.0.6
4910 *
4911 * @param string|number $id_or_slug
4912 *
4913 * @return string
4914 */
4915 function get_addon_basename( $id_or_slug ) {
4916 $addon_id = self::get_module_id( $id_or_slug );
4917
4918 if ( $this->is_addon_activated( $addon_id ) ) {
4919 return self::instance( $addon_id )->get_plugin_basename();
4920 }
4921
4922 $addon = $this->get_addon( $addon_id );
4923 $premium_basename = "{$addon->slug}-premium/{$addon->slug}.php";
4924
4925 if ( file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $premium_basename ) ) ) {
4926 return $premium_basename;
4927 }
4928
4929 $all_plugins = $this->get_all_plugins();
4930
4931 foreach ( $all_plugins as $basename => &$data ) {
4932 if ( $addon->slug === $data['slug'] ||
4933 $addon->slug . '-premium' === $data['slug']
4934 ) {
4935 return $basename;
4936 }
4937 }
4938
4939 $free_basename = "{$addon->slug}/{$addon->slug}.php";
4940
4941 return $free_basename;
4942 }
4943
4944 /**
4945 * Get installed add-ons instances.
4946 *
4947 * @author Vova Feldman (@svovaf)
4948 * @since 1.0.6
4949 *
4950 * @return Freemius[]
4951 */
4952 function get_installed_addons() {
4953 $installed_addons = array();
4954 foreach ( self::$_instances as $instance ) {
4955 if ( $instance->is_addon() && is_object( $instance->_parent_plugin ) ) {
4956 if ( $this->_plugin->id == $instance->_parent_plugin->id ) {
4957 $installed_addons[] = $instance;
4958 }
4959 }
4960 }
4961
4962 return $installed_addons;
4963 }
4964
4965 /**
4966 * Check if any add-ons of the plugin are installed.
4967 *
4968 * @author Leo Fajardo (@leorw)
4969 * @since 1.1.1
4970 *
4971 * @return bool
4972 */
4973 function has_installed_addons() {
4974 if ( ! $this->has_addons() ) {
4975 return false;
4976 }
4977
4978 foreach ( self::$_instances as $instance ) {
4979 if ( $instance->is_addon() && is_object( $instance->_parent_plugin ) ) {
4980 if ( $this->_plugin->id == $instance->_parent_plugin->id ) {
4981 return true;
4982 }
4983 }
4984 }
4985
4986 return false;
4987 }
4988
4989 /**
4990 * Tell Freemius that the current plugin is an add-on.
4991 *
4992 * @author Vova Feldman (@svovaf)
4993 * @since 1.0.6
4994 *
4995 * @param number $parent_plugin_id The parent plugin ID
4996 */
4997 function init_addon( $parent_plugin_id ) {
4998 $this->_plugin->parent_plugin_id = $parent_plugin_id;
4999 }
5000
5001 /**
5002 * @author Vova Feldman (@svovaf)
5003 * @since 1.0.6
5004 *
5005 * @return bool
5006 */
5007 function is_addon() {
5008 return isset( $this->_plugin->parent_plugin_id ) && is_numeric( $this->_plugin->parent_plugin_id );
5009 }
5010
5011 /**
5012 * Deactivate add-on if it's premium only and the user does't have a valid license.
5013 *
5014 * @param bool $is_after_trial_cancel
5015 *
5016 * @return bool If add-on was deactivated.
5017 */
5018 private function deactivate_premium_only_addon_without_license( $is_after_trial_cancel = false ) {
5019 if ( ! $this->has_free_plan() &&
5020 ! $this->has_features_enabled_license() &&
5021 ! $this->_has_premium_license()
5022 ) {
5023 if ( $this->is_registered() ) {
5024 // IF wrapper is turned off because activation_timestamp is currently only stored for plugins (not addons).
5025 // if (empty($this->_storage->activation_timestamp) ||
5026 // (WP_FS__SCRIPT_START_TIME - $this->_storage->activation_timestamp) > 30
5027 // ) {
5028 /**
5029 * @todo When it's first fail, there's no reason to try and re-sync because the licenses were just synced after initial activation.
5030 *
5031 * Retry syncing the user add-on licenses.
5032 */
5033 // Sync licenses.
5034 $this->_sync_licenses();
5035 // }
5036
5037 // Try to activate premium license.
5038 $this->_activate_license( true );
5039 }
5040
5041 if ( ! $this->has_free_plan() &&
5042 ! $this->has_features_enabled_license() &&
5043 ! $this->_has_premium_license()
5044 ) {
5045 // @todo Check if deactivate plugins also call the deactivation hook.
5046
5047 $this->_parent->_admin_notices->add_sticky(
5048 sprintf(
5049 ( $is_after_trial_cancel ?
5050 $this->_parent->get_text_inline(
5051 '%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.',
5052 'addon-trial-cancelled-message'
5053 ) :
5054 $this->_parent->get_text_inline(
5055 '%s is a premium only add-on. You have to purchase a license first before activating the plugin.',
5056 'addon-no-license-message'
5057 )
5058 ),
5059 '<b>' . $this->_plugin->title . '</b>'
5060 ) . ' ' . sprintf(
5061 '<a href="%s" aria-label="%s" class="button button-primary" style="margin-left: 10px; vertical-align: middle;">%s &nbsp;&#10140;</a>',
5062 $this->_parent->addon_url( $this->_slug ),
5063 esc_attr( sprintf( $this->_parent->get_text_inline( 'More information about %s', 'more-information-about-x' ), $this->_plugin->title ) ),
5064 $this->_parent->get_text_inline( 'Purchase License', 'purchase-license' )
5065 ),
5066 'no_addon_license_' . $this->_slug,
5067 ( $is_after_trial_cancel ? '' : $this->_parent->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...' ),
5068 ( $is_after_trial_cancel ? 'success' : 'error' )
5069 );
5070
5071 deactivate_plugins( array( $this->_plugin_basename ), true );
5072
5073 return true;
5074 }
5075 }
5076
5077 return false;
5078 }
5079
5080 #endregion
5081
5082 #----------------------------------------------------------------------------------
5083 #region Sandbox
5084 #----------------------------------------------------------------------------------
5085
5086 /**
5087 * Set Freemius into sandbox mode for debugging.
5088 *
5089 * @author Vova Feldman (@svovaf)
5090 * @since 1.0.4
5091 *
5092 * @param string $secret_key
5093 */
5094 function init_sandbox( $secret_key ) {
5095 $this->_plugin->secret_key = $secret_key;
5096
5097 // Update plugin details.
5098 FS_Plugin_Manager::instance( $this->_module_id )->update( $this->_plugin, true );
5099 }
5100
5101 /**
5102 * Check if running payments in sandbox mode.
5103 *
5104 * @author Vova Feldman (@svovaf)
5105 * @since 1.0.4
5106 *
5107 * @return bool
5108 */
5109 function is_payments_sandbox() {
5110 return ( ! $this->is_live() ) || isset( $this->_plugin->secret_key );
5111 }
5112
5113 #endregion
5114
5115 /**
5116 * Check if running test vs. live plugin.
5117 *
5118 * @author Vova Feldman (@svovaf)
5119 * @since 1.0.5
5120 *
5121 * @return bool
5122 */
5123 function is_live() {
5124 return $this->_plugin->is_live;
5125 }
5126
5127 /**
5128 * Check if super-admin skipped connection for all sites in the network.
5129 *
5130 * @author Vova Feldman (@svovaf)
5131 * @since 2.0.0
5132 */
5133 function is_network_anonymous() {
5134 if ( ! $this->_is_network_active ) {
5135 return false;
5136 }
5137
5138 $is_anonymous_ms = $this->_storage->get( 'is_anonymous_ms' );
5139
5140 if ( empty( $is_anonymous_ms ) ) {
5141 return false;
5142 }
5143
5144 return $is_anonymous_ms['is'];
5145 }
5146
5147 /**
5148 * Check if super-admin opted-in for all sites in the network.
5149 *
5150 * @author Vova Feldman (@svovaf)
5151 * @since 2.0.0
5152 */
5153 function is_network_connected() {
5154 if ( ! $this->_is_network_active ) {
5155 return false;
5156 }
5157
5158 return $this->_storage->get( 'is_network_connected' );
5159 }
5160
5161 /**
5162 * Check if the user skipped connecting the account with Freemius.
5163 *
5164 * @author Vova Feldman (@svovaf)
5165 * @since 1.0.7
5166 *
5167 * @return bool
5168 */
5169 function is_anonymous() {
5170 if ( ! isset( $this->_is_anonymous ) ) {
5171 if ( $this->is_network_anonymous() ) {
5172 $this->_is_anonymous = true;
5173 } else {
5174 if ( ! isset( $this->_storage->is_anonymous ) ) {
5175 // Not skipped.
5176 $this->_is_anonymous = false;
5177 } else if ( is_bool( $this->_storage->is_anonymous ) ) {
5178 // For back compatibility, since the variable was boolean before.
5179 $this->_is_anonymous = $this->_storage->is_anonymous;
5180
5181 // Upgrade stored data format to 1.1.3 format.
5182 $this->set_anonymous_mode( $this->_storage->is_anonymous );
5183 } else {
5184 // Version 1.1.3 and later.
5185 $this->_is_anonymous = $this->_storage->is_anonymous['is'];
5186 }
5187 }
5188 }
5189
5190 return $this->_is_anonymous;
5191 }
5192
5193 /**
5194 * Check if the user skipped the connection of a specified site.
5195 *
5196 * @author Vova Feldman (@svovaf)
5197 * @since 2.0.0
5198 *
5199 * @param int $blog_id
5200 *
5201 * @return bool
5202 */
5203 function is_anonymous_site( $blog_id = 0 ) {
5204 if ( $this->is_network_anonymous() ) {
5205 return true;
5206 }
5207
5208 $is_anonymous = $this->_storage->get( 'is_anonymous', false, $blog_id );
5209
5210 if ( empty( $is_anonymous ) ) {
5211 return false;
5212 }
5213
5214 return $is_anonymous['is'];
5215 }
5216
5217 /**
5218 * Check if user connected his account and install pending email activation.
5219 *
5220 * @author Vova Feldman (@svovaf)
5221 * @since 1.0.7
5222 *
5223 * @return bool
5224 */
5225 function is_pending_activation() {
5226 return $this->_storage->get( 'is_pending_activation', false );
5227 }
5228
5229 /**
5230 * Check if plugin must be WordPress.org compliant.
5231 *
5232 * @since 1.0.7
5233 *
5234 * @return bool
5235 */
5236 function is_org_repo_compliant() {
5237 return $this->_is_org_compliant;
5238 }
5239
5240 #--------------------------------------------------------------------------------
5241 #region WP Cron Common
5242 #--------------------------------------------------------------------------------
5243
5244 /**
5245 * @author Vova Feldman (@svovaf)
5246 * @since 2.0.0
5247 *
5248 * @param string $name Cron name.
5249 *
5250 * @return object
5251 */
5252 private function get_cron_data( $name ) {
5253 $this->_logger->entrance( $name );
5254
5255 /**
5256 * @var object $cron_data
5257 */
5258 return $this->_storage->get( "{$name}_cron", null );
5259 }
5260
5261 /**
5262 * @author Vova Feldman (@svovaf)
5263 * @since 2.0.0
5264 *
5265 * @param string $name Cron name.
5266 */
5267 private function clear_cron_data( $name ) {
5268 $this->_logger->entrance( $name );
5269
5270 $this->_storage->remove( "{$name}_cron" );
5271 }
5272
5273 /**
5274 * @author Vova Feldman (@svovaf)
5275 * @since 2.0.0
5276 *
5277 * @param string $name Cron name.
5278 * @param int $cron_blog_id The cron executing blog ID.
5279 */
5280 private function set_cron_data( $name, $cron_blog_id = 0 ) {
5281 $this->_logger->entrance( $name );
5282
5283 $this->_storage->store( "{$name}_cron", (object) array(
5284 'version' => $this->get_plugin_version(),
5285 'blog_id' => $cron_blog_id,
5286 'sdk_version' => $this->version,
5287 'timestamp' => WP_FS__SCRIPT_START_TIME,
5288 'on' => true,
5289 ) );
5290 }
5291
5292 /**
5293 * Get the cron's executing blog ID.
5294 *
5295 * @author Vova Feldman (@svovaf)
5296 * @since 2.0.0
5297 *
5298 * @param string $name Cron name.
5299 *
5300 * @return int
5301 */
5302 private function get_cron_blog_id( $name ) {
5303 $this->_logger->entrance( $name );
5304
5305 /**
5306 * @var object $cron_data
5307 */
5308 $cron_data = $this->get_cron_data( $name );
5309
5310 return ( is_object( $cron_data ) && is_numeric( $cron_data->blog_id ) ) ?
5311 $cron_data->blog_id :
5312 0;
5313 }
5314
5315 /**
5316 * @author Vova Feldman (@svovaf)
5317 * @since 2.0.0
5318 *
5319 * @param string $name Cron name.
5320 *
5321 * @return bool
5322 */
5323 private function is_cron_on( $name ) {
5324 $this->_logger->entrance( $name );
5325
5326 /**
5327 * @var object $cron_data
5328 */
5329 $cron_data = $this->get_cron_data( $name );
5330
5331 return ( ! is_null( $cron_data ) && true === $cron_data->on );
5332 }
5333
5334 /**
5335 * Unix timestamp for previous cron execution or false if never executed.
5336 *
5337 * @author Vova Feldman (@svovaf)
5338 * @since 2.0.0
5339 *
5340 * @param string $name Cron name.
5341 *
5342 * @return int|false
5343 */
5344 private function cron_last_execution( $name ) {
5345 $this->_logger->entrance( $name );
5346
5347 return $this->_storage->get( "{$name}_timestamp" );
5348 }
5349
5350 /**
5351 * Set cron execution time to now.
5352 *
5353 * @author Vova Feldman (@svovaf)
5354 * @since 2.0.0
5355 *
5356 * @param string $name Cron name.
5357 */
5358 private function set_cron_execution_timestamp( $name ) {
5359 $this->_logger->entrance( $name );
5360
5361 $this->_storage->store( "{$name}_timestamp", time() );
5362 }
5363
5364 /**
5365 * Check if cron was executed in the last $period of seconds.
5366 *
5367 * @author Vova Feldman (@svovaf)
5368 * @since 2.0.0
5369 *
5370 * @param string $name Cron name.
5371 * @param int $period In seconds
5372 *
5373 * @return bool
5374 */
5375 private function is_cron_executed( $name, $period = WP_FS__TIME_24_HOURS_IN_SEC ) {
5376 $this->_logger->entrance( $name );
5377
5378 $last_execution = $this->set_cron_execution_timestamp( $name );
5379
5380 if ( ! is_numeric( $last_execution ) ) {
5381 return false;
5382 }
5383
5384 return ( $last_execution > ( WP_FS__SCRIPT_START_TIME - $period ) );
5385 }
5386
5387 /**
5388 * WP Cron is executed on a site level. When running in a multisite network environment
5389 * with the network integration activated, for optimization reasons, we are consolidating
5390 * the installs data sync cron to be executed only from a single site.
5391 *
5392 * @author Vova Feldman (@svovaf)
5393 * @since 2.0.0
5394 *
5395 * @param int $except_blog_id Target any except the excluded blog ID.
5396 *
5397 * @return int
5398 */
5399 private function get_cron_target_blog_id( $except_blog_id = 0 ) {
5400 if ( ! is_multisite() ) {
5401 return 0;
5402 }
5403
5404 if ( $this->_is_network_active &&
5405 is_numeric( $this->_storage->network_install_blog_id ) &&
5406 $except_blog_id != $this->_storage->network_install_blog_id &&
5407 self::is_site_active( $this->_storage->network_install_blog_id )
5408 ) {
5409 // Try to run cron from the main network blog.
5410 $install = $this->get_install_by_blog_id( $this->_storage->network_install_blog_id );
5411
5412 if ( is_object( $install ) &&
5413 ( $this->is_premium() || $install->is_tracking_allowed() )
5414 ) {
5415 return $this->_storage->network_install_blog_id;
5416 }
5417 }
5418
5419 // Get first opted-in blog ID with active tracking.
5420 $installs = $this->get_blog_install_map();
5421 foreach ( $installs as $blog_id => $install ) {
5422 if ( $except_blog_id != $blog_id &&
5423 self::is_site_active( $blog_id ) &&
5424 ( $this->is_premium() || $install->is_tracking_allowed() )
5425 ) {
5426 return $blog_id;
5427 }
5428 }
5429
5430 return 0;
5431 }
5432
5433 /**
5434 * @author Vova Feldman (@svovaf)
5435 * @since 2.0.0
5436 *
5437 * @param string $name Cron name.
5438 * @param string $action_tag Callback action tag.
5439 * @param bool $is_network_clear If set to TRUE, clear sync cron even if there are installs that are still connected.
5440 */
5441 private function clear_cron( $name, $action_tag = '', $is_network_clear = false ) {
5442 $this->_logger->entrance( $name );
5443
5444 if ( ! $this->is_cron_on( $name ) ) {
5445 return;
5446 }
5447
5448 $clear_cron = true;
5449 if ( ! $is_network_clear && $this->_is_network_active ) {
5450 $installs = $this->get_blog_install_map();
5451
5452 foreach ( $installs as $blog_id => $install ) {
5453 /**
5454 * @var FS_Site $install
5455 */
5456 if ( $install->is_tracking_allowed() ) {
5457 $clear_cron = false;
5458 break;
5459 }
5460 }
5461 }
5462
5463 if ( ! $clear_cron ) {
5464 return;
5465 }
5466
5467 /**
5468 * @var object $cron_data
5469 */
5470 $cron_data = $this->get_cron_data( $name );
5471
5472 $cron_blog_id = is_object( $cron_data ) && isset( $cron_data->blog_id ) ?
5473 $cron_data->blog_id :
5474 0;
5475
5476 $this->clear_cron_data( $name );
5477
5478 if ( 0 < $cron_blog_id ) {
5479 switch_to_blog( $cron_blog_id );
5480 }
5481
5482 if ( empty( $action_tag ) ) {
5483 $action_tag = $name;
5484 }
5485
5486 wp_clear_scheduled_hook( $this->get_action_tag( $action_tag ) );
5487
5488 if ( 0 < $cron_blog_id ) {
5489 restore_current_blog();
5490 }
5491 }
5492
5493 /**
5494 * Unix timestamp for next cron execution or false if not scheduled.
5495 *
5496 * @author Vova Feldman (@svovaf)
5497 * @since 2.0.0
5498 *
5499 * @param string $name Cron name.
5500 * @param string $action_tag Callback action tag.
5501 *
5502 * @return int|false
5503 */
5504 private function get_next_scheduled_cron( $name, $action_tag = '' ) {
5505 $this->_logger->entrance( $name );
5506
5507 if ( ! $this->is_cron_on( $name ) ) {
5508 return false;
5509 }
5510
5511 /**
5512 * @var object $cron_data
5513 */
5514 $cron_data = $this->get_cron_data( $name );
5515
5516 $cron_blog_id = is_object( $cron_data ) && isset( $cron_data->blog_id ) ?
5517 $cron_data->blog_id :
5518 0;
5519
5520 if ( 0 < $cron_blog_id ) {
5521 switch_to_blog( $cron_blog_id );
5522 }
5523
5524 if ( empty( $action_tag ) ) {
5525 $action_tag = $name;
5526 }
5527
5528 $next_scheduled = wp_next_scheduled( $this->get_action_tag( $action_tag ) );
5529
5530 if ( 0 < $cron_blog_id ) {
5531 restore_current_blog();
5532 }
5533
5534 return $next_scheduled;
5535 }
5536
5537 /**
5538 * @author Vova Feldman (@svovaf)
5539 * @since 2.0.0
5540 *
5541 * @param string $name Cron name.
5542 * @param string $action_tag Callback action tag.
5543 * @param string $recurrence 'single' or 'daily'.
5544 * @param int $start_at Defaults to now.
5545 * @param bool $randomize_start If true, schedule first job randomly during the next 12 hours. Otherwise, schedule job to start right away.
5546 * @param int $except_blog_id Target any except the excluded blog ID.
5547 */
5548 private function schedule_cron(
5549 $name,
5550 $action_tag = '',
5551 $recurrence = 'single',
5552 $start_at = WP_FS__SCRIPT_START_TIME,
5553 $randomize_start = true,
5554 $except_blog_id = 0
5555 ) {
5556 $this->_logger->entrance( $name );
5557
5558 $this->clear_cron( $name, $action_tag, true );
5559
5560 $cron_blog_id = $this->get_cron_target_blog_id( $except_blog_id );
5561
5562 if ( is_multisite() && 0 == $cron_blog_id ) {
5563 // Don't schedule cron since couldn't find a target blog.
5564 return;
5565 }
5566
5567 if ( 0 < $cron_blog_id ) {
5568 switch_to_blog( $cron_blog_id );
5569 }
5570
5571 if ( 'daily' === $recurrence ) {
5572 if ( $randomize_start ) {
5573 // Schedule first sync with a random 12 hour time range from now.
5574 $start_at += rand( 0, ( WP_FS__TIME_24_HOURS_IN_SEC / 2 ) );
5575 }
5576
5577 // Schedule daily WP cron.
5578 wp_schedule_event(
5579 $start_at,
5580 'daily',
5581 $this->get_action_tag( $action_tag )
5582 );
5583 } else if ( 'single' === $recurrence ) {
5584 // Schedule single cron.
5585 wp_schedule_single_event(
5586 $start_at,
5587 $this->get_action_tag( $action_tag )
5588 );
5589 }
5590
5591 $this->set_cron_data( $name, $cron_blog_id );
5592
5593 if ( 0 < $cron_blog_id ) {
5594 restore_current_blog();
5595 }
5596 }
5597
5598 /**
5599 * Consolidated cron execution for performance optimization. The max number of API requests is based on the number of unique opted-in users.
5600 * that doesn't halt page loading.
5601 *
5602 * @author Vova Feldman (@svovaf)
5603 * @since 2.0.0
5604 *
5605 * @param string $name Cron name.
5606 * @param callable $callable The function that should be executed.
5607 */
5608 private function execute_cron( $name, $callable ) {
5609 $this->_logger->entrance( $name );
5610
5611 // Store the last time data sync was executed.
5612 $this->set_cron_execution_timestamp( $name );
5613
5614 // Check if API is temporary down.
5615 if ( FS_Api::is_temporary_down() ) {
5616 return;
5617 }
5618
5619 // @todo Add logic that identifies API latency, and reschedule the next background sync randomly between 8-16 hours.
5620
5621 $users_2_blog_ids = array();
5622
5623 if ( ! is_multisite() ) {
5624 // Add dummy blog.
5625 $users_2_blog_ids[0] = array( 0 );
5626 } else {
5627 $installs = $this->get_blog_install_map();
5628 foreach ( $installs as $blog_id => $install ) {
5629 if ( $this->is_premium() || $install->is_tracking_allowed() ) {
5630 if ( ! isset( $users_2_blog_ids[ $install->user_id ] ) ) {
5631 $users_2_blog_ids[ $install->user_id ] = array();
5632 }
5633
5634 $users_2_blog_ids[ $install->user_id ][] = $blog_id;
5635 }
5636 }
5637 }
5638
5639 foreach ( $users_2_blog_ids as $user_id => $blog_ids ) {
5640 if ( 0 < $blog_ids[0] ) {
5641 $this->switch_to_blog( $blog_ids[0] );
5642 }
5643
5644 call_user_func_array( $callable, array( $blog_ids ) );
5645
5646 foreach ( $blog_ids as $blog_id ) {
5647 $this->do_action( "after_{$name}_cron", $blog_id );
5648 }
5649 }
5650
5651 if ( is_multisite() ) {
5652 $this->do_action( "after_{$name}_cron_multisite" );
5653 }
5654 }
5655
5656 #endregion
5657
5658 #----------------------------------------------------------------------------------
5659 #region Daily Sync Cron
5660 #----------------------------------------------------------------------------------
5661
5662
5663 /**
5664 * @author Vova Feldman (@svovaf)
5665 * @since 2.0.0
5666 *
5667 * @return bool
5668 */
5669 private function is_sync_cron_scheduled() {
5670 return $this->is_cron_on( 'sync' );
5671 }
5672
5673 /**
5674 * Get the sync cron's executing blog ID.
5675 *
5676 * @author Vova Feldman (@svovaf)
5677 * @since 2.0.0
5678 *
5679 * @return int
5680 */
5681 private function get_sync_cron_blog_id() {
5682 return $this->get_cron_blog_id( 'sync' );
5683 }
5684
5685 /**
5686 * @author Vova Feldman (@svovaf)
5687 * @since 1.1.7.3
5688 */
5689 private function run_manual_sync() {
5690 self::require_pluggable_essentials();
5691
5692 if ( ! $this->is_user_admin() ) {
5693 return;
5694 }
5695
5696 // Run manual sync.
5697 $this->_sync_cron();
5698
5699 // Reschedule next cron to run 24 hours from now (performance optimization).
5700 $this->schedule_sync_cron( time() + WP_FS__TIME_24_HOURS_IN_SEC, false );
5701 }
5702
5703 /**
5704 * Data sync cron job. Replaces the background sync non blocking HTTP request
5705 * that doesn't halt page loading.
5706 *
5707 * @author Vova Feldman (@svovaf)
5708 * @since 1.1.7.3
5709 * @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.
5710 */
5711 function _sync_cron() {
5712 $this->_logger->entrance();
5713
5714 $this->execute_cron( 'sync', array( &$this, '_sync_cron_method' ) );
5715 }
5716
5717 /**
5718 * The actual data sync cron logic.
5719 *
5720 * @author Vova Feldman (@svovaf)
5721 * @since 2.0.0
5722 *
5723 * @param int[] $blog_ids
5724 */
5725 function _sync_cron_method( array $blog_ids ) {
5726 if ( $this->is_registered() ) {
5727 if ( $this->has_paid_plan() ) {
5728 // Initiate background plan sync.
5729 $this->_sync_license( true );
5730
5731 if ( $this->is_paying() ) {
5732 // Check for premium plugin updates.
5733 $this->check_updates( true );
5734 }
5735 } else {
5736 // Sync install(s) (only if something changed locally).
5737 if ( 1 < count( $blog_ids ) ) {
5738 $this->sync_installs();
5739 } else {
5740 $this->sync_install();
5741 }
5742 }
5743 }
5744 }
5745
5746 /**
5747 * Check if sync was executed in the last $period of seconds.
5748 *
5749 * @author Vova Feldman (@svovaf)
5750 * @since 1.1.7.3
5751 *
5752 * @param int $period In seconds
5753 *
5754 * @return bool
5755 */
5756 private function is_sync_executed( $period = WP_FS__TIME_24_HOURS_IN_SEC ) {
5757 return $this->is_cron_executed( 'sync', $period );
5758 }
5759
5760 /**
5761 * @author Vova Feldman (@svovaf)
5762 * @since 1.1.7.3
5763 *
5764 * @return bool
5765 */
5766 private function is_sync_cron_on() {
5767 return $this->is_cron_on( 'sync' );
5768 }
5769
5770 /**
5771 * @author Vova Feldman (@svovaf)
5772 * @since 1.1.7.3
5773 *
5774 * @param int $start_at Defaults to now.
5775 * @param bool $randomize_start If true, schedule first job randomly during the next 12 hours. Otherwise, schedule job to start right away.
5776 * @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.
5777 */
5778 private function schedule_sync_cron(
5779 $start_at = WP_FS__SCRIPT_START_TIME,
5780 $randomize_start = true,
5781 $except_blog_id = 0
5782 ) {
5783 $this->schedule_cron(
5784 'sync',
5785 'data_sync',
5786 'daily',
5787 $start_at,
5788 $randomize_start,
5789 $except_blog_id
5790 );
5791 }
5792
5793 /**
5794 * Add the actual sync function to the cron job hook.
5795 *
5796 * @author Vova Feldman (@svovaf)
5797 * @since 1.1.7.3
5798 */
5799 private function hook_callback_to_sync_cron() {
5800 $this->add_action( 'data_sync', array( &$this, '_sync_cron' ) );
5801 }
5802
5803 /**
5804 * @author Vova Feldman (@svovaf)
5805 * @since 1.1.7.3
5806 *
5807 * @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.
5808 */
5809 private function clear_sync_cron( $is_network_clear = false ) {
5810 $this->_logger->entrance();
5811
5812 $this->clear_cron( 'sync', 'data_sync', $is_network_clear );
5813 }
5814
5815 /**
5816 * Unix timestamp for next sync cron execution or false if not scheduled.
5817 *
5818 * @author Vova Feldman (@svovaf)
5819 * @since 1.1.7.3
5820 *
5821 * @return int|false
5822 */
5823 function next_sync_cron() {
5824 return $this->get_next_scheduled_cron( 'sync', 'data_sync' );
5825 }
5826
5827 /**
5828 * Unix timestamp for previous sync cron execution or false if never executed.
5829 *
5830 * @author Vova Feldman (@svovaf)
5831 * @since 1.1.7.3
5832 *
5833 * @return int|false
5834 */
5835 function last_sync_cron() {
5836 return $this->cron_last_execution( 'sync' );
5837 }
5838
5839 #endregion Daily Sync Cron ------------------------------------------------------------------
5840
5841 #----------------------------------------------------------------------------------
5842 #region Async Install Sync
5843 #----------------------------------------------------------------------------------
5844
5845 /**
5846 * @author Vova Feldman (@svovaf)
5847 * @since 1.1.7.3
5848 *
5849 * @return bool
5850 */
5851 private function is_install_sync_scheduled() {
5852 return $this->is_cron_on( 'install_sync' );
5853 }
5854
5855 /**
5856 * Get the sync cron's executing blog ID.
5857 *
5858 * @author Vova Feldman (@svovaf)
5859 * @since 2.0.0
5860 *
5861 * @return int
5862 */
5863 private function get_install_sync_cron_blog_id() {
5864 return $this->get_cron_blog_id( 'install_sync' );
5865 }
5866
5867 /**
5868 * Instead of running blocking install sync event, execute non blocking scheduled wp-cron.
5869 *
5870 * @author Vova Feldman (@svovaf)
5871 * @since 1.1.7.3
5872 *
5873 * @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.
5874 */
5875 private function schedule_install_sync( $except_blog_id = 0 ) {
5876 $this->schedule_cron( 'install_sync', 'install_sync', 'single', 0, false, $except_blog_id );
5877 }
5878
5879 /**
5880 * Unix timestamp for previous install sync cron execution or false if never executed.
5881 *
5882 * @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.
5883 *
5884 * @author Vova Feldman (@svovaf)
5885 * @since 1.1.7.3
5886 *
5887 * @return int|false
5888 */
5889 function last_install_sync() {
5890 return $this->cron_last_execution( 'install_sync' );
5891 }
5892
5893 /**
5894 * Unix timestamp for next install sync cron execution or false if not scheduled.
5895 *
5896 * @author Vova Feldman (@svovaf)
5897 * @since 1.1.7.3
5898 *
5899 * @return int|false
5900 */
5901 function next_install_sync() {
5902 return $this->get_next_scheduled_cron( 'install_sync', 'install_sync' );
5903 }
5904
5905 /**
5906 * Add the actual install sync function to the cron job hook.
5907 *
5908 * @author Vova Feldman (@svovaf)
5909 * @since 1.1.7.3
5910 */
5911 private function hook_callback_to_install_sync() {
5912 $this->add_action( 'install_sync', array( &$this, '_run_sync_install' ) );
5913 }
5914
5915 /**
5916 * @author Vova Feldman (@svovaf)
5917 * @since 1.1.7.3
5918 *
5919 * @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.
5920 */
5921 private function clear_install_sync_cron( $is_network_clear = false ) {
5922 $this->_logger->entrance();
5923
5924 $this->clear_cron( 'install_sync', 'install_sync', $is_network_clear );
5925 }
5926
5927 /**
5928 * @author Vova Feldman (@svovaf)
5929 * @since 1.1.7.3
5930 * @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.
5931 */
5932 public function _run_sync_install() {
5933 $this->_logger->entrance();
5934
5935 $this->execute_cron( 'sync', array( &$this, '_sync_install_cron_method' ) );
5936 }
5937
5938 /**
5939 * The actual install(s) sync cron logic.
5940 *
5941 * @author Vova Feldman (@svovaf)
5942 * @since 2.0.0
5943 *
5944 * @param int[] $blog_ids
5945 */
5946 function _sync_install_cron_method( array $blog_ids ) {
5947 if ( $this->is_registered() ) {
5948 if ( 1 < count( $blog_ids ) ) {
5949 $this->sync_installs( array(), true );
5950 } else {
5951 $this->sync_install( array(), true );
5952 }
5953 }
5954 }
5955
5956 #endregion Async Install Sync ------------------------------------------------------------------
5957
5958 /**
5959 * Show a notice that activation is currently pending.
5960 *
5961 * @author Vova Feldman (@svovaf)
5962 * @since 1.0.7
5963 *
5964 * @param bool|string $email
5965 * @param bool $is_pending_trial Since 1.2.1.5
5966 */
5967 function _add_pending_activation_notice( $email = false, $is_pending_trial = false ) {
5968 if ( ! is_string( $email ) ) {
5969 $current_user = self::_get_current_wp_user();
5970 $email = $current_user->user_email;
5971 }
5972
5973 $this->_admin_notices->add_sticky(
5974 sprintf(
5975 $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' ),
5976 '<b>' . $this->get_plugin_name() . '</b>',
5977 '<b>' . $email . '</b>',
5978 ( $is_pending_trial ?
5979 $this->get_text_inline( 'start the trial', 'start-the-trial' ) :
5980 $this->get_text_inline( 'complete the install', 'complete-the-install' ) )
5981 ),
5982 'activation_pending',
5983 'Thanks!'
5984 );
5985 }
5986
5987 /**
5988 * Check if currently in plugin activation.
5989 *
5990 * @author Vova Feldman (@svovaf)
5991 * @since 1.1.4
5992 *
5993 * @return bool
5994 */
5995 function is_plugin_activation() {
5996 return get_option( 'fs_'
5997 . ( $this->is_plugin() ? '' : $this->_module_type . '_' )
5998 . "{$this->_slug}_activated", false );
5999 }
6000
6001 /**
6002 *
6003 * NOTE: admin_menu action executed before admin_init.
6004 *
6005 * @author Vova Feldman (@svovaf)
6006 * @since 1.0.7
6007 */
6008 function _admin_init_action() {
6009 /**
6010 * Automatically redirect to connect/activation page after plugin activation.
6011 *
6012 * @since 1.1.7 Do NOT redirect to opt-in when running in network admin mode.
6013 */
6014 if ( $this->is_plugin_activation() ) {
6015 delete_option( 'fs_'
6016 . ( $this->is_plugin() ? '' : $this->_module_type . '_' )
6017 . "{$this->_slug}_activated" );
6018
6019 $this->_redirect_on_activation_hook();
6020
6021 return;
6022 }
6023
6024 if ( fs_request_is_action( $this->get_unique_affix() . '_skip_activation' ) ) {
6025 check_admin_referer( $this->get_unique_affix() . '_skip_activation' );
6026
6027 $this->skip_connection( null, fs_is_network_admin() );
6028
6029 fs_redirect( $this->get_after_activation_url( 'after_skip_url' ) );
6030 }
6031
6032 if ( $this->is_network_activation_mode() &&
6033 fs_request_is_action( $this->get_unique_affix() . '_delegate_activation' )
6034 ) {
6035 check_admin_referer( $this->get_unique_affix() . '_delegate_activation' );
6036
6037 $this->delegate_connection();
6038
6039 fs_redirect( $this->get_after_activation_url( 'after_delegation_url' ) );
6040 }
6041
6042 if ( ! $this->is_addon() &&
6043 (
6044 // Not registered nor anonymous.
6045 ( ! $this->is_registered() && ! $this->is_anonymous() ) ||
6046 // OR, network level and in network upgrade mode.
6047 ( fs_is_network_admin() && $this->_is_network_active && $this->is_network_upgrade_mode() )
6048 )
6049 ) {
6050 if ( ! $this->is_pending_activation() ) {
6051 if ( ! $this->_menu->is_main_settings_page() ) {
6052 /**
6053 * If a user visits any other admin page before activating the premium-only theme with a valid
6054 * license, reactivate the previous theme.
6055 *
6056 * @author Leo Fajardo (@leorw)
6057 * @since 1.2.2
6058 */
6059 if ( $this->is_theme()
6060 && $this->is_only_premium()
6061 && ! $this->has_settings_menu()
6062 && ! isset( $_REQUEST['fs_action'] )
6063 && $this->can_activate_previous_theme()
6064 ) {
6065 $this->activate_previous_theme();
6066
6067 return;
6068 }
6069
6070 if ( ! fs_is_network_admin() &&
6071 $this->is_network_activation_mode() &&
6072 ! $this->is_delegated_connection()
6073 ) {
6074 return;
6075 }
6076
6077 if ( $this->is_plugin_new_install() || $this->is_only_premium() ) {
6078 // Show notice for new plugin installations.
6079 $this->_admin_notices->add(
6080 sprintf(
6081 $this->get_text_inline( 'You are just one step away - %s', 'you-are-step-away' ),
6082 sprintf( '<b><a href="%s">%s</a></b>',
6083 $this->get_activation_url( array(), ! $this->is_delegated_connection() ),
6084 sprintf( $this->get_text_x_inline( 'Complete "%s" Activation Now',
6085 '%s - plugin name. As complete "PluginX" activation now', 'activate-x-now' ), $this->get_plugin_name() )
6086 )
6087 ),
6088 '',
6089 'update-nag'
6090 );
6091 } else {
6092 if ( $this->should_add_sticky_optin_notice() ) {
6093 $this->add_sticky_optin_admin_notice();
6094 }
6095
6096 if ( $this->has_filter( 'optin_pointer_element' ) ) {
6097 // Don't show admin nag if plugin update.
6098 wp_enqueue_script( 'wp-pointer' );
6099 wp_enqueue_style( 'wp-pointer' );
6100
6101 $this->_enqueue_connect_essentials();
6102
6103 add_action( 'admin_print_footer_scripts', array(
6104 $this,
6105 '_add_connect_pointer_script'
6106 ) );
6107 }
6108 }
6109 }
6110 }
6111
6112 if ( $this->is_theme() &&
6113 $this->_menu->is_main_settings_page()
6114 ) {
6115 $this->_show_theme_activation_optin_dialog();
6116 }
6117 }
6118
6119 $this->_add_upgrade_action_link();
6120 }
6121
6122 /**
6123 * @author Vova Feldman (@svovaf)
6124 * @since 2.0.0
6125 *
6126 * @return bool
6127 */
6128 private function should_add_sticky_optin_notice() {
6129 if ( fs_is_network_admin() ) {
6130 if ( ! $this->_is_network_active ) {
6131 return false;
6132 }
6133
6134 if ( ! $this->is_network_activation_mode() ) {
6135 return false;
6136 }
6137
6138 return ! isset( $this->_storage->sticky_optin_added_ms );
6139 }
6140
6141 if ( ! $this->is_activation_mode() ) {
6142 return false;
6143 }
6144
6145 // If running from a blog admin and delegated the connection.
6146 return ! isset( $this->_storage->sticky_optin_added );
6147 }
6148
6149 /**
6150 * @author Leo Fajardo (@leorw)
6151 * @since 2.0.0
6152 */
6153 private function add_sticky_optin_admin_notice() {
6154 if ( ! $this->_is_network_active || ! fs_is_network_admin() ) {
6155 $this->_storage->sticky_optin_added = true;
6156 } else {
6157 $this->_storage->sticky_optin_added_ms = true;
6158 }
6159
6160 // Show notice for new plugin installations.
6161 $this->_admin_notices->add_sticky(
6162 sprintf(
6163 $this->get_text_inline( 'We made a few tweaks to the %s, %s', 'few-plugin-tweaks' ),
6164 $this->_module_type,
6165 sprintf( '<b><a href="%s">%s</a></b>',
6166 $this->get_activation_url(),
6167 sprintf( $this->get_text_inline( 'Opt in to make "%s" Better!', 'optin-x-now' ), $this->get_plugin_name() )
6168 )
6169 ),
6170 'connect_account',
6171 '',
6172 'update-nag'
6173 );
6174 }
6175
6176 /**
6177 * Enqueue connect requires scripts and styles.
6178 *
6179 * @author Vova Feldman (@svovaf)
6180 * @since 1.1.4
6181 */
6182 function _enqueue_connect_essentials() {
6183 wp_enqueue_script( 'jquery' );
6184 wp_enqueue_script( 'json2' );
6185
6186 fs_enqueue_local_script( 'postmessage', 'nojquery.ba-postmessage.min.js' );
6187 fs_enqueue_local_script( 'fs-postmessage', 'postmessage.js' );
6188
6189 fs_enqueue_local_style( 'fs_connect', '/admin/connect.css' );
6190 }
6191
6192 /**
6193 * Add connect / opt-in pointer.
6194 *
6195 * @author Vova Feldman (@svovaf)
6196 * @since 1.1.4
6197 */
6198 function _add_connect_pointer_script() {
6199 $vars = array( 'id' => $this->_module_id );
6200 $pointer_content = fs_get_template( 'connect.php', $vars );
6201 ?>
6202 <script type="text/javascript">// <![CDATA[
6203 jQuery(document).ready(function ($) {
6204 if ('undefined' !== typeof(jQuery().pointer)) {
6205
6206 var element = <?php echo $this->apply_filters( 'optin_pointer_element', '$("#non_existing_element");' ) ?>;
6207
6208 if (element.length > 0) {
6209 var optin = $(element).pointer($.extend(true, {}, {
6210 content : <?php echo json_encode( $pointer_content ) ?>,
6211 position : {
6212 edge : 'left',
6213 align: 'center'
6214 },
6215 buttons : function () {
6216 // Don't show pointer buttons.
6217 return '';
6218 },
6219 pointerWidth: 482
6220 }, <?php echo $this->apply_filters( 'optin_pointer_options_json', '{}' ) ?>));
6221
6222 <?php
6223 echo $this->apply_filters( 'optin_pointer_execute', "
6224
6225 optin.pointer('open');
6226
6227 // Tag the opt-in pointer with custom class.
6228 $('.wp-pointer #fs_connect')
6229 .parents('.wp-pointer.wp-pointer-top')
6230 .addClass('fs-opt-in-pointer');
6231
6232 ", 'element', 'optin' ) ?>
6233 }
6234 }
6235 });
6236 // ]]></script>
6237 <?php
6238 }
6239
6240 /**
6241 * Return current page's URL.
6242 *
6243 * @author Vova Feldman (@svovaf)
6244 * @since 1.0.7
6245 *
6246 * @return string
6247 */
6248 function current_page_url() {
6249 $url = 'http';
6250
6251 if ( isset( $_SERVER["HTTPS"] ) ) {
6252 if ( $_SERVER["HTTPS"] == "on" ) {
6253 $url .= "s";
6254 }
6255 }
6256 $url .= "://";
6257 if ( $_SERVER["SERVER_PORT"] != "80" ) {
6258 $url .= $_SERVER["SERVER_NAME"] . ":" . $_SERVER["SERVER_PORT"] . $_SERVER["REQUEST_URI"];
6259 } else {
6260 $url .= $_SERVER["SERVER_NAME"] . $_SERVER["REQUEST_URI"];
6261 }
6262
6263 return esc_url( $url );
6264 }
6265
6266 /**
6267 * Check if the current page is the plugin's main admin settings page.
6268 *
6269 * @author Vova Feldman (@svovaf)
6270 * @since 1.0.7
6271 *
6272 * @return bool
6273 */
6274 function _is_plugin_page() {
6275 return fs_is_plugin_page( $this->_menu->get_raw_slug() ) ||
6276 fs_is_plugin_page( $this->_slug );
6277 }
6278
6279 /* Events
6280 ------------------------------------------------------------------------------------------------------------------*/
6281 /**
6282 * Delete site install from Database.
6283 *
6284 * @author Vova Feldman (@svovaf)
6285 * @since 1.0.1
6286 *
6287 * @param bool $store
6288 * @param int|null $blog_id Since 2.0.0
6289 *
6290 * @return false|int The install ID if deleted. Otherwise, FALSE (when install not exist).
6291 */
6292 function _delete_site( $store = true, $blog_id = null ) {
6293 return self::_delete_site_by_slug( $this->_slug, $this->_module_type, $store, $blog_id );
6294 }
6295
6296 /**
6297 * Delete site install from Database.
6298 *
6299 * @author Vova Feldman (@svovaf)
6300 * @since 1.2.2.7
6301 *
6302 * @param string $slug
6303 * @param string $module_type
6304 * @param bool $store
6305 * @param int|null $blog_id Since 2.0.0
6306 *
6307 * @return false|int The install ID if deleted. Otherwise, FALSE (when install not exist).
6308 */
6309 static function _delete_site_by_slug( $slug, $module_type, $store = true, $blog_id = null ) {
6310 $sites = self::get_all_sites( $module_type, $blog_id );
6311
6312 $install_id = false;
6313
6314 if ( isset( $sites[ $slug ] ) ) {
6315 if ( is_object( $sites[ $slug ] ) ) {
6316 $install_id = $sites[ $slug ]->id;
6317 }
6318
6319 unset( $sites[ $slug ] );
6320
6321 self::set_account_option_by_module( $module_type, 'sites', $sites, $store, $blog_id );
6322 }
6323
6324 return $install_id;
6325 }
6326
6327 /**
6328 * Delete user.
6329 *
6330 * @author Vova Feldman (@svovaf)
6331 * @since 2.0.0
6332 *
6333 * @param number $user_id
6334 * @param bool $store
6335 *
6336 * @return false|int The user ID if deleted. Otherwise, FALSE (when install not exist).
6337 */
6338 private static function delete_user( $user_id, $store = true ) {
6339 $users = self::get_all_users();
6340
6341 if ( ! is_array( $users ) || ! isset( $users[ $user_id ] ) ) {
6342 return false;
6343 }
6344
6345 unset( $users[ $user_id ] );
6346
6347 self::$_accounts->set_option( 'users', $users, $store );
6348
6349 return $user_id;
6350 }
6351
6352 /**
6353 * Delete plugin's plans information.
6354 *
6355 * @param bool $store Flush to Database if true.
6356 * @param bool $keep_associated_plans If set to false, delete all plans, even if a plan is associated with an install.
6357 *
6358 * @author Vova Feldman (@svovaf)
6359 * @since 1.0.9
6360 */
6361 private function _delete_plans( $store = true, $keep_associated_plans = true ) {
6362 $this->_logger->entrance();
6363
6364 $plans = self::get_all_plans( $this->_module_type );
6365
6366 $plans_to_keep = array();
6367
6368 if ( $keep_associated_plans ) {
6369 $plans_ids_to_keep = $this->get_plans_ids_associated_with_installs();
6370 foreach ( $plans_ids_to_keep as $plan_id ) {
6371 $plan = self::_get_plan_by_id( $plan_id );
6372 if ( is_object( $plan ) ) {
6373 $plans_to_keep[] = $plan;
6374 }
6375 }
6376 }
6377
6378 if ( ! empty( $plans_to_keep ) ) {
6379 $plans[ $this->_slug ] = $plans_to_keep;
6380 } else {
6381 unset( $plans[ $this->_slug ] );
6382 }
6383
6384 $this->set_account_option( 'plans', $plans, $store );
6385 }
6386
6387 /**
6388 * Delete all plugin licenses.
6389 *
6390 * @author Vova Feldman (@svovaf)
6391 * @since 1.0.9
6392 *
6393 * @param bool $store
6394 */
6395 private function _delete_licenses( $store = true ) {
6396 $this->_logger->entrance();
6397
6398 $all_licenses = self::get_all_licenses();
6399
6400 unset( $all_licenses[ $this->_module_id ] );
6401
6402 self::$_accounts->set_option( 'all_licenses', $all_licenses, $store );
6403 }
6404
6405 /**
6406 * Check if Freemius was added on new plugin installation.
6407 *
6408 * @author Vova Feldman (@svovaf)
6409 * @since 1.1.5
6410 *
6411 * @return bool
6412 */
6413 function is_plugin_new_install() {
6414 return isset( $this->_storage->is_plugin_new_install ) &&
6415 $this->_storage->is_plugin_new_install;
6416 }
6417
6418 /**
6419 * Check if it's the first plugin release that is running Freemius.
6420 *
6421 * @author Vova Feldman (@svovaf)
6422 * @since 1.2.1.5
6423 *
6424 * @return bool
6425 */
6426 function is_first_freemius_powered_version() {
6427 return empty( $this->_storage->plugin_last_version );
6428 }
6429
6430 /**
6431 * @author Leo Fajardo (@leorw)
6432 * @since 1.2.2
6433 *
6434 * @return bool|string
6435 */
6436 private function get_previous_theme_slug() {
6437 return isset( $this->_storage->previous_theme ) ?
6438 $this->_storage->previous_theme :
6439 false;
6440 }
6441
6442 /**
6443 * @author Leo Fajardo (@leorw)
6444 * @since 1.2.2
6445 *
6446 * @return string
6447 */
6448 private function can_activate_previous_theme() {
6449 $slug = $this->get_previous_theme_slug();
6450 if ( false !== $slug && current_user_can( 'switch_themes' ) ) {
6451 $theme_instance = wp_get_theme( $slug );
6452
6453 return $theme_instance->exists();
6454 }
6455
6456 return false;
6457 }
6458
6459 /**
6460 * @author Leo Fajardo (@leorw)
6461 * @since 1.2.2
6462 *
6463 * @return string
6464 */
6465 private function activate_previous_theme() {
6466 switch_theme( $this->get_previous_theme_slug() );
6467 unset( $this->_storage->previous_theme );
6468
6469 global $pagenow;
6470 if ( 'themes.php' === $pagenow ) {
6471 /**
6472 * Refresh the active theme information.
6473 *
6474 * @author Leo Fajardo (@leorw)
6475 * @since 1.2.2
6476 */
6477 fs_redirect( $this->admin_url( $pagenow ) );
6478 }
6479 }
6480
6481 /**
6482 * @author Leo Fajardo (@leorw)
6483 * @since 1.2.2
6484 *
6485 * @return string
6486 */
6487 function get_previous_theme_activation_url() {
6488 if ( ! $this->can_activate_previous_theme() ) {
6489 return '';
6490 }
6491
6492 /**
6493 * Activation URL
6494 *
6495 * @author Leo Fajardo (@leorw)
6496 * @since 1.2.2
6497 */
6498 return wp_nonce_url(
6499 $this->admin_url( 'themes.php?action=activate&stylesheet=' . urlencode( $this->get_previous_theme_slug() ) ),
6500 'switch-theme_' . $this->get_previous_theme_slug()
6501 );
6502 }
6503
6504 /**
6505 * Saves the slug of the previous theme if it still exists so that it can be used by the logic in the opt-in
6506 * form that decides whether to add a close button to the opt-in dialog or not. So after a premium-only theme is
6507 * activated, the close button will appear and will reactivate the previous theme if clicked. If the previous
6508 * theme doesn't exist, then there will be no close button.
6509 *
6510 * @author Leo Fajardo (@leorw)
6511 * @since 1.2.2
6512 *
6513 * @param string $slug_or_name Old theme's slug or name.
6514 * @param bool|WP_Theme $old_theme WP_Theme instance of the old theme if it still exists.
6515 */
6516 function _activate_theme_event_hook( $slug_or_name, $old_theme = false ) {
6517 $this->_storage->previous_theme = ( false !== $old_theme ) ?
6518 $old_theme->get_stylesheet() :
6519 $slug_or_name;
6520
6521 $this->_activate_plugin_event_hook();
6522 }
6523
6524 /**
6525 * Plugin activated hook.
6526 *
6527 * @author Vova Feldman (@svovaf)
6528 * @since 1.0.1
6529 *
6530 * @uses FS_Api
6531 */
6532 function _activate_plugin_event_hook() {
6533 $this->_logger->entrance( 'slug = ' . $this->_slug );
6534
6535 if ( ! $this->is_user_admin() ) {
6536 return;
6537 }
6538
6539 $this->unregister_uninstall_hook();
6540
6541 // Clear API cache on activation.
6542 FS_Api::clear_cache();
6543
6544 $is_premium_version_activation = ( current_filter() !== ( 'activate_' . $this->_free_plugin_basename ) );
6545
6546 $this->_logger->info( 'Activating ' . ( $is_premium_version_activation ? 'premium' : 'free' ) . ' plugin version.' );
6547
6548 // 1. If running in the activation of the FREE module, get the basename of the PREMIUM.
6549 // 2. If running in the activation of the PREMIUM module, get the basename of the FREE.
6550 $other_version_basename = $is_premium_version_activation ?
6551 $this->_free_plugin_basename :
6552 $this->premium_plugin_basename();
6553
6554 if ( ! $this->_is_network_active ) {
6555 /**
6556 * During the activation, the plugin isn't yet active, therefore,
6557 * _is_network_active will be set to false even if it's a network level
6558 * activation. So we need to fix that by looking at the is_network_admin() value.
6559 *
6560 * @author Vova Feldman
6561 */
6562 $this->_is_network_active = (
6563 $this->_is_multisite_integrated &&
6564 // Themes are always network activated, but the ACTUAL activation is per site.
6565 $this->is_plugin() &&
6566 fs_is_network_admin()
6567 );
6568 }
6569
6570 /**
6571 * If the other module version is activate, deactivate it.
6572 *
6573 * is_plugin_active() checks if the plugin active on the site or the network level
6574 * and deactivate_plugins() deactivates the plugin whether its activated on the site
6575 * or network level.
6576 *
6577 * @author Leo Fajardo (@leorw)
6578 * @since 1.2.2
6579 */
6580 if ( is_plugin_active( $other_version_basename ) ) {
6581 deactivate_plugins( $other_version_basename );
6582 }
6583
6584 if ( $this->is_registered() ) {
6585 if ( $is_premium_version_activation ) {
6586 $this->reconnect_locally();
6587 }
6588
6589
6590 // Schedule re-activation event and sync.
6591 // $this->sync_install( array(), true );
6592 $this->schedule_install_sync();
6593
6594 // If activating the premium module version, add an admin notice to congratulate for an upgrade completion.
6595 if ( $is_premium_version_activation ) {
6596 $this->_admin_notices->add(
6597 sprintf( $this->get_text_inline( 'The upgrade of %s was successfully completed.', 'successful-version-upgrade-message' ), sprintf( '<b>%s</b>', $this->_plugin->title ) ),
6598 $this->get_text_x_inline( 'W00t',
6599 'Used to express elation, enthusiasm, or triumph (especially in electronic communication).', 'woot' ) . '!'
6600 );
6601 }
6602 } else if ( $this->is_anonymous() ) {
6603 if ( isset( $this->_storage->is_anonymous_ms ) && $this->_storage->is_anonymous_ms['is'] ) {
6604 $plugin_version = $this->_storage->is_anonymous_ms['version'];
6605 $network = true;
6606 } else {
6607 $plugin_version = $this->_storage->is_anonymous['version'];
6608 $network = false;
6609 }
6610
6611 /**
6612 * Reset "skipped" click cache on the following:
6613 * 1. Freemius DEV mode.
6614 * 2. WordPress DEBUG mode.
6615 * 3. If a plugin and the user skipped the exact same version before.
6616 *
6617 * @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).
6618 *
6619 * @todo 4. If explicitly asked to retry after every activation.
6620 */
6621 if ( WP_FS__DEV_MODE ||
6622 (
6623 ( $this->is_plugin() || ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ) &&
6624 $this->get_plugin_version() == $plugin_version
6625 )
6626 ) {
6627 $this->reset_anonymous_mode( $network );
6628 }
6629 }
6630
6631 if ( ! isset( $this->_storage->is_plugin_new_install ) ) {
6632 /**
6633 * If no previous version of plugin's version exist, it means that it's either
6634 * the first time that the plugin installed on the site, or the plugin was installed
6635 * before but didn't have Freemius integrated.
6636 *
6637 * Since register_activation_hook() do NOT fires on updates since 3.1, and only fires
6638 * on manual activation via the dashboard, is_plugin_activation() is TRUE
6639 * only after immediate activation.
6640 *
6641 * @since 1.1.4
6642 * @link https://make.wordpress.org/core/2010/10/27/plugin-activation-hooks-no-longer-fire-for-updates/
6643 */
6644 $this->_storage->is_plugin_new_install = empty( $this->_storage->plugin_last_version );
6645 }
6646
6647 if ( ! $this->_anonymous_mode &&
6648 $this->has_api_connectivity( WP_FS__DEV_MODE ) &&
6649 ! $this->_isAutoInstall
6650 ) {
6651 // Store hint that the plugin was just activated to enable auto-redirection to settings.
6652 add_option( 'fs_'
6653 . ( $this->is_plugin() ? '' : $this->_module_type . '_' )
6654 . "{$this->_slug}_activated", true );
6655 }
6656
6657 /**
6658 * Activation hook is executed after the plugin's main file is loaded, therefore,
6659 * after the plugin was loaded. The logic is located at activate_plugin()
6660 * ./wp-admin/includes/plugin.php.
6661 *
6662 * @author Vova Feldman (@svovaf)
6663 * @since 1.1.9
6664 */
6665 $this->_storage->was_plugin_loaded = true;
6666 }
6667
6668 /**
6669 * Delete account.
6670 *
6671 * @author Vova Feldman (@svovaf)
6672 * @since 1.0.3
6673 *
6674 * @param bool $check_user Enforce checking if user have plugins activation privileges.
6675 */
6676 function delete_account_event( $check_user = true ) {
6677 $this->_logger->entrance( 'slug = ' . $this->_slug );
6678
6679 if ( $check_user && ! $this->is_user_admin() ) {
6680 return;
6681 }
6682
6683 $this->do_action( 'before_account_delete' );
6684
6685 // Clear all admin notices.
6686 $this->_admin_notices->clear_all_sticky( false );
6687
6688 $this->_delete_site( false );
6689
6690 $delete_network_common_data = true;
6691
6692 if ( $this->_is_network_active ) {
6693 $installs = $this->get_blog_install_map();
6694
6695 // Don't delete common network data unless no other installs left.
6696 $delete_network_common_data = empty( $installs );
6697 }
6698
6699 if ( $delete_network_common_data ) {
6700 $this->_delete_plans( false );
6701
6702 $this->_delete_licenses( false );
6703
6704 // Delete add-ons related to plugin's account.
6705 $this->_delete_account_addons( false );
6706 }
6707
6708 // @todo Delete plans and licenses of add-ons.
6709
6710 self::$_accounts->store();
6711
6712 /**
6713 * IMPORTANT:
6714 * Clear crons must be executed before clearing all storage.
6715 * Otherwise, the cron will not be cleared.
6716 */
6717 if ( $delete_network_common_data ) {
6718 $this->clear_sync_cron();
6719 }
6720
6721 $this->clear_install_sync_cron();
6722
6723 // Clear all storage data.
6724 $this->_storage->clear_all( true, array(
6725 'connectivity_test',
6726 'is_on',
6727 ), false );
6728
6729 // Send delete event.
6730 $this->get_api_site_scope()->call( '/', 'delete' );
6731
6732 $this->do_action( 'after_account_delete' );
6733 }
6734
6735 /**
6736 * Delete network level account.
6737 *
6738 * @author Vova Feldman (@svovaf)
6739 * @since 2.0.0
6740 *
6741 * @param bool $check_user Enforce checking if user have plugins activation privileges.
6742 */
6743 function delete_network_account_event( $check_user = true ) {
6744 $this->_logger->entrance( 'slug = ' . $this->_slug );
6745
6746 if ( $check_user && ! $this->is_user_admin() ) {
6747 return;
6748 }
6749
6750 $this->do_action( 'before_network_account_delete' );
6751
6752 // Clear all admin notices.
6753 $this->_admin_notices->clear_all_sticky();
6754
6755 $this->_delete_plans( false, false );
6756
6757 $this->_delete_licenses( false );
6758
6759 // Delete add-ons related to plugin's account.
6760 $this->_delete_account_addons( false );
6761
6762 // @todo Delete plans and licenses of add-ons.
6763
6764 self::$_accounts->store( true );
6765
6766 /**
6767 * IMPORTANT:
6768 * Clear crons must be executed before clearing all storage.
6769 * Otherwise, the cron will not be cleared.
6770 */
6771 $this->clear_sync_cron( true );
6772 $this->clear_install_sync_cron( true );
6773
6774 $sites = self::get_sites();
6775
6776 $install_ids = array();
6777 foreach ( $sites as $site ) {
6778 $blog_id = self::get_site_blog_id( $site );
6779
6780 $install_id = $this->_delete_site( true, $blog_id );
6781
6782 // Clear all storage data.
6783 $this->_storage->clear_all( true, array( 'connectivity_test' ), $blog_id );
6784
6785 if ( FS_Site::is_valid_id( $install_id ) ) {
6786 $install_ids[] = $install_id;
6787 }
6788
6789 switch_to_blog( $blog_id );
6790
6791 $this->do_action( 'after_account_delete' );
6792
6793 restore_current_blog();
6794 }
6795
6796 $this->_storage->clear_all( true, array(
6797 'connectivity_test',
6798 'is_on',
6799 ), true );
6800
6801 // Send delete event.
6802 if ( ! empty( $install_ids ) ) {
6803 $result = $this->get_current_or_network_user_api_scope()->call( "/plugins/{$this->_module_id}/installs.json?ids=" . implode( ',', $install_ids ), 'delete' );
6804 }
6805
6806 $this->do_action( 'after_network_account_delete' );
6807 }
6808
6809 /**
6810 * Plugin deactivation hook.
6811 *
6812 * @author Vova Feldman (@svovaf)
6813 * @since 1.0.1
6814 */
6815 function _deactivate_plugin_hook() {
6816 $this->_logger->entrance( 'slug = ' . $this->_slug );
6817
6818 if ( ! $this->is_user_admin() ) {
6819 return;
6820 }
6821
6822 $is_network_deactivation = fs_is_network_admin();
6823 $storage_keys_for_removal = array();
6824
6825 $this->_admin_notices->clear_all_sticky();
6826
6827 $storage_keys_for_removal[] = 'sticky_optin_added';
6828 if ( isset( $this->_storage->sticky_optin_added ) ) {
6829 unset( $this->_storage->sticky_optin_added );
6830 }
6831
6832 if ( ! isset( $this->_storage->is_plugin_new_install ) ) {
6833 // Remember that plugin was already installed.
6834 $this->_storage->is_plugin_new_install = false;
6835 }
6836
6837 // Hook to plugin uninstall.
6838 register_uninstall_hook( $this->_plugin_main_file_path, array( 'Freemius', '_uninstall_plugin_hook' ) );
6839
6840 $this->clear_module_main_file_cache();
6841 $this->clear_sync_cron( $this->_is_network_active );
6842 $this->clear_install_sync_cron();
6843
6844 if ( $this->is_registered() ) {
6845 if ( $is_network_deactivation ) {
6846 // Send deactivation event.
6847 $this->sync_installs( array(
6848 'is_active' => false,
6849 ) );
6850 } else {
6851 // Send deactivation event.
6852 $this->sync_install( array(
6853 'is_active' => false,
6854 ) );
6855 }
6856 } else {
6857 if ( ! $this->has_api_connectivity() ) {
6858 // Reset connectivity test cache.
6859 unset( $this->_storage->connectivity_test );
6860
6861 $storage_keys_for_removal[] = 'connectivity_test';
6862 }
6863 }
6864
6865 if ( $is_network_deactivation ) {
6866 if ( isset( $this->_storage->sticky_optin_added_ms ) ) {
6867 unset( $this->_storage->sticky_optin_added_ms );
6868 }
6869
6870 if ( ! empty( $storage_keys_for_removal ) ) {
6871 $sites = self::get_sites();
6872
6873 foreach ( $sites as $site ) {
6874 $blog_id = self::get_site_blog_id( $site );
6875
6876 foreach ( $storage_keys_for_removal as $key ) {
6877 $this->_storage->remove( $key, false, $blog_id );
6878 }
6879
6880 $this->_storage->save( $blog_id );
6881 }
6882 }
6883 }
6884
6885 // Clear API cache on deactivation.
6886 FS_Api::clear_cache();
6887
6888 $this->remove_sdk_reference();
6889 }
6890
6891 /**
6892 * @author Vova Feldman (@svovaf)
6893 * @since 1.1.6
6894 */
6895 private function remove_sdk_reference() {
6896 global $fs_active_plugins;
6897
6898 foreach ( $fs_active_plugins->plugins as $sdk_path => &$data ) {
6899 if ( $this->_plugin_basename == $data->plugin_path ) {
6900 unset( $fs_active_plugins->plugins[ $sdk_path ] );
6901 break;
6902 }
6903 }
6904
6905 fs_fallback_to_newest_active_sdk();
6906 }
6907
6908 /**
6909 * @author Vova Feldman (@svovaf)
6910 * @since 1.1.3
6911 *
6912 * @param bool $is_anonymous
6913 * @param bool|int $network_or_blog_id Since 2.0.0
6914 */
6915 private function set_anonymous_mode( $is_anonymous = true, $network_or_blog_id = 0 ) {
6916 // Store information regarding skip to try and opt-in the user
6917 // again in the future.
6918 $skip_info = array(
6919 'is' => $is_anonymous,
6920 'timestamp' => WP_FS__SCRIPT_START_TIME,
6921 'version' => $this->get_plugin_version(),
6922 );
6923
6924 if ( true === $network_or_blog_id ) {
6925 $this->_storage->is_anonymous_ms = $skip_info;
6926 } else {
6927 $this->_storage->store( 'is_anonymous', $skip_info, $network_or_blog_id );
6928 }
6929
6930 $this->network_upgrade_mode_completed();
6931
6932 // Update anonymous mode cache.
6933 $this->_is_anonymous = $is_anonymous;
6934 }
6935
6936 /**
6937 * @author Vova Feldman (@svovaf)
6938 * @since 2.0.0
6939 *
6940 * @param int $blog_id Site ID.
6941 * @param int $user_id User ID.
6942 * @param string $domain Site domain.
6943 * @param string $path Site path.
6944 * @param int $network_id Network ID. Only relevant on multi-network installations.
6945 * @param array $meta Metadata. Used to set initial site options.
6946 *
6947 * @uses Freemius::is_license_network_active() to check if the context license was network activated by the super-admin.
6948 * @uses Freemius::is_network_connected() to check if the super-admin network opted-in.
6949 * @uses Freemius::is_network_anonymous() to check if the super-admin network skipped.
6950 * @uses Freemius::is_network_delegated_connection() to check if the super-admin network delegated the connection to the site admins.
6951 */
6952 function _after_new_blog_callback( $blog_id, $user_id, $domain, $path, $network_id, $meta ) {
6953 $this->_logger->entrance();
6954
6955 if ( $this->is_premium() &&
6956 $this->is_network_connected() &&
6957 is_object( $this->_license ) &&
6958 $this->_license->can_activate( FS_Site::is_localhost_by_address( $domain ) ) &&
6959 $this->is_license_network_active( $blog_id )
6960 ) {
6961 /**
6962 * 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.
6963 */
6964 $current_blog_id = get_current_blog_id();
6965 $license = clone $this->_license;
6966
6967 $this->switch_to_blog( $blog_id );
6968
6969 // Opt-in with network user.
6970 $this->install_with_user(
6971 $this->get_network_user(),
6972 $license->secret_key,
6973 false,
6974 false,
6975 false
6976 );
6977
6978 if ( is_object( $this->_site ) ) {
6979 if ( $this->_site->license_id == $license->id ) {
6980 /**
6981 * If the license was activated successfully, sync the license data from the remote server.
6982 */
6983 $this->_license = $license;
6984 $this->sync_site_license();
6985 }
6986 }
6987
6988 $this->switch_to_blog( $current_blog_id );
6989
6990 if ( is_object( $this->_site ) ) {
6991 // Already connected (with or without a license), so no need to continue.
6992 return;
6993 }
6994 }
6995
6996 if ( $this->is_network_anonymous() ) {
6997 /**
6998 * Opt-in was network skipped so automatically skip the opt-in for the new site.
6999 */
7000 $this->skip_site_connection( $blog_id );
7001 } else if ( $this->is_network_delegated_connection() ) {
7002 /**
7003 * Opt-in was network delegated so automatically delegate the opt-in for the new site's admin.
7004 */
7005 $this->delegate_site_connection( $blog_id );
7006 } else if ( $this->is_network_connected() ) {
7007 /**
7008 * Opt-in was network activated so automatically opt-in with the network user and new site admin.
7009 */
7010 $current_blog_id = get_current_blog_id();
7011
7012 $this->switch_to_blog( $blog_id );
7013
7014 // Opt-in with network user.
7015 $this->install_with_user(
7016 $this->get_network_user(),
7017 false,
7018 false,
7019 false,
7020 false
7021 );
7022
7023 $this->switch_to_blog( $current_blog_id );
7024 } else {
7025 /**
7026 * If the super-admin mixed different options (connect, skip, delegated):
7027 * a) If at least one site connection was delegated, then automatically delegate connection.
7028 * 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.
7029 */
7030 $has_delegated_site = false;
7031
7032 $sites = self::get_sites();
7033 foreach ( $sites as $site ) {
7034 $blog_id = self::get_site_blog_id( $site );
7035
7036 if ( $this->is_site_delegated_connection( $blog_id ) ) {
7037 $has_delegated_site = true;
7038 break;
7039 }
7040 }
7041
7042 if ( $has_delegated_site ) {
7043 $this->delegate_site_connection( $blog_id );
7044 } else {
7045 $this->skip_site_connection( $blog_id );
7046 }
7047 }
7048 }
7049
7050 /**
7051 * @author Vova Feldman (@svovaf)
7052 * @since 1.1.3
7053 *
7054 * @param bool|int $network_or_blog_id Since 2.0.0.
7055 */
7056 private function reset_anonymous_mode( $network_or_blog_id = 0 ) {
7057 if ( true === $network_or_blog_id ) {
7058 unset( $this->_storage->is_anonymous_ms );
7059 } else {
7060 $this->_storage->remove( 'is_anonymous', true, $network_or_blog_id );
7061 }
7062
7063 /**
7064 * Ensure that this field is also "false", otherwise, if the current module's type is "theme" and the module
7065 * has no menus, the opt-in popup will not be shown immediately (in this case, the user will have to click
7066 * on the admin notice that contains the opt-in link in order to trigger the opt-in popup).
7067 *
7068 * @author Leo Fajardo (@leorw)
7069 * @since 1.2.2
7070 */
7071 if ( ! $this->_is_network_active ||
7072 0 === $network_or_blog_id ||
7073 get_current_blog_id() == $network_or_blog_id ||
7074 ( true === $network_or_blog_id && fs_is_network_admin() )
7075 ) {
7076 unset( $this->_is_anonymous );
7077 }
7078 }
7079
7080 /**
7081 * Clears the anonymous mode and redirects to the opt-in screen.
7082 *
7083 * @author Vova Feldman (@svovaf)
7084 * @since 1.1.7
7085 */
7086 function connect_again() {
7087 if ( ! $this->is_anonymous() ) {
7088 return;
7089 }
7090
7091 $this->reset_anonymous_mode( fs_is_network_admin() );
7092
7093 fs_redirect( $this->get_activation_url() );
7094 }
7095
7096 /**
7097 * Skip account connect, and set anonymous mode.
7098 *
7099 * @author Vova Feldman (@svovaf)
7100 * @since 1.1.1
7101 *
7102 * @param array|null $sites Since 2.0.0. Specific sites.
7103 * @param bool $skip_all_network Since 2.0.0. If true, skip connection for all sites.
7104 */
7105 private function skip_connection( $sites = null, $skip_all_network = false ) {
7106 $this->_logger->entrance();
7107
7108 $this->_admin_notices->remove_sticky( 'connect_account' );
7109
7110 if ( $skip_all_network ) {
7111 $this->set_anonymous_mode( true, true );
7112 }
7113
7114 if ( ! $skip_all_network && empty( $sites ) ) {
7115 $this->skip_site_connection();
7116 } else {
7117 $uids = array();
7118
7119 if ( $skip_all_network ) {
7120 $this->set_anonymous_mode( true, true );
7121
7122 $sites = self::get_sites();
7123 foreach ( $sites as $site ) {
7124 $blog_id = self::get_site_blog_id( $site );
7125 $this->skip_site_connection( $blog_id, false );
7126 $uids[] = $this->get_anonymous_id( $blog_id );
7127 }
7128 } else if ( ! empty( $sites ) ) {
7129 foreach ( $sites as $site ) {
7130 $uids[] = $site['uid'];
7131 $this->skip_site_connection( $site['blog_id'], false );
7132 }
7133 }
7134
7135 // Send anonymous skip event.
7136 // No user identified info nor any tracking will be sent after the user skips the opt-in.
7137 $this->get_api_plugin_scope()->call( 'skip.json', 'put', array(
7138 'uids' => $uids,
7139 ) );
7140 }
7141
7142 $this->network_upgrade_mode_completed();
7143 }
7144
7145 /**
7146 * Skip connection for specific site in the network.
7147 *
7148 * @author Vova Feldman (@svovaf)
7149 * @since 2.0.0
7150 *
7151 * @param int|null $blog_id
7152 * @param bool $send_skip
7153 */
7154 private function skip_site_connection( $blog_id = null, $send_skip = true ) {
7155 $this->_logger->entrance();
7156
7157 $this->_admin_notices->remove_sticky( 'connect_account', $blog_id );
7158
7159 $this->set_anonymous_mode( true, $blog_id );
7160
7161 if ( $send_skip ) {
7162 $this->get_api_plugin_scope()->call( 'skip.json', 'put', array(
7163 'uids' => array( $this->get_anonymous_id( $blog_id ) ),
7164 ) );
7165 }
7166 }
7167
7168 /**
7169 * Plugin version update hook.
7170 *
7171 * @author Vova Feldman (@svovaf)
7172 * @since 1.0.4
7173 */
7174 private function update_plugin_version_event() {
7175 $this->_logger->entrance();
7176
7177 if ( ! $this->is_registered() ) {
7178 return;
7179 }
7180
7181 $this->schedule_install_sync();
7182 // $this->sync_install( array(), true );
7183 }
7184
7185 /**
7186 * Generate an MD5 signature of a plugins collection.
7187 * This helper methods used to identify changes in a plugins collection.
7188 *
7189 * @author Vova Feldman (@svovaf)
7190 * @since 2.0.0
7191 *
7192 * @param array [string]array $plugins
7193 *
7194 * @return string
7195 */
7196 private function get_plugins_thumbprint( $plugins ) {
7197 ksort( $plugins );
7198
7199 $thumbprint = '';
7200 foreach ( $plugins as $basename => $data ) {
7201 $thumbprint .= $data['slug'] . ',' .
7202 $data['Version'] . ',' .
7203 ( $data['is_active'] ? '1' : '0' ) . ';';
7204 }
7205
7206 return md5( $thumbprint );
7207 }
7208
7209 /**
7210 * Return a list of modified plugins since the last sync.
7211 *
7212 * Note:
7213 * There's no point to store a plugins counter since even if the number of
7214 * plugins didn't change, we still need to check if the versions are all the
7215 * same and the activity state is similar.
7216 *
7217 * @author Vova Feldman (@svovaf)
7218 * @since 1.1.8
7219 *
7220 * @return array|false
7221 */
7222 private function get_plugins_data_for_api() {
7223 // Alias.
7224 $site_active_plugins_option_name = 'active_plugins';
7225 $network_plugins_option_name = 'all_plugins';
7226
7227 /**
7228 * Collection of all site level active plugins.
7229 */
7230 $site_active_plugins_cache = self::$_accounts->get_option( $site_active_plugins_option_name );
7231
7232 if ( ! is_object( $site_active_plugins_cache ) ) {
7233 $site_active_plugins_cache = (object) array(
7234 'timestamp' => '',
7235 'md5' => '',
7236 'plugins' => array(),
7237 );
7238 }
7239
7240 $time = time();
7241
7242 if ( ! empty( $site_active_plugins_cache->timestamp ) &&
7243 ( $time - $site_active_plugins_cache->timestamp ) < WP_FS__TIME_5_MIN_IN_SEC
7244 ) {
7245 // Don't send plugin updates if last update was in the past 5 min.
7246 return false;
7247 }
7248
7249 // Write timestamp to lock the logic.
7250 $site_active_plugins_cache->timestamp = $time;
7251 self::$_accounts->set_option( $site_active_plugins_option_name, $site_active_plugins_cache, true );
7252
7253 // Reload options from DB.
7254 self::$_accounts->load( true );
7255 $site_active_plugins_cache = self::$_accounts->get_option( $site_active_plugins_option_name );
7256
7257 if ( $time != $site_active_plugins_cache->timestamp ) {
7258 // If timestamp is different, then another thread captured the lock.
7259 return false;
7260 }
7261
7262 /**
7263 * Collection of all plugins (network level).
7264 */
7265 $network_plugins_cache = self::$_accounts->get_option( $network_plugins_option_name );
7266
7267 if ( ! is_object( $network_plugins_cache ) ) {
7268 $network_plugins_cache = (object) array(
7269 'timestamp' => '',
7270 'md5' => '',
7271 'plugins' => array(),
7272 );
7273 }
7274
7275 // Check if there's a change in plugins.
7276 $network_plugins = self::get_network_plugins();
7277 $site_active_plugins = self::get_site_active_plugins();
7278
7279 $network_plugins_thumbprint = $this->get_plugins_thumbprint( $network_plugins );
7280 $site_active_plugins_thumbprint = $this->get_plugins_thumbprint( $site_active_plugins );
7281
7282 // Check if plugins status changed (version or active/inactive).
7283 $network_plugins_changed = ( $network_plugins_cache->md5 !== $network_plugins_thumbprint );
7284 $site_active_plugins_changed = ( $site_active_plugins_cache->md5 !== $site_active_plugins_thumbprint );
7285
7286 if ( ! $network_plugins_changed &&
7287 ! $site_active_plugins_changed
7288 ) {
7289 // No changes.
7290 return array();
7291 }
7292
7293 $plugins_update_data = array();
7294
7295 foreach ( $network_plugins_cache->plugins as $basename => $data ) {
7296 if ( ! isset( $network_plugins[ $basename ] ) ) {
7297 // Plugin uninstalled.
7298 $uninstalled_plugin_data = $data;
7299 $uninstalled_plugin_data['is_active'] = false;
7300 $uninstalled_plugin_data['is_uninstalled'] = true;
7301 $plugins_update_data[] = $uninstalled_plugin_data;
7302
7303 unset( $network_plugins[ $basename ] );
7304
7305 unset( $network_plugins_cache->plugins[ $basename ] );
7306 unset( $site_active_plugins_cache->plugins[ $basename ] );
7307
7308 continue;
7309 }
7310
7311 $was_active = $data['is_active'] ||
7312 ( isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
7313 true === $site_active_plugins_cache->plugins[ $basename ]['is_active'] );
7314 $is_active = $network_plugins[ $basename ]['is_active'] ||
7315 ( isset( $site_active_plugins[ $basename ] ) &&
7316 $site_active_plugins[ $basename ]['is_active'] );
7317
7318 if ( ! isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
7319 isset( $site_active_plugins[ $basename ] )
7320 ) {
7321 // Plugin was site level activated.
7322 $site_active_plugins_cache->plugins[ $basename ] = $network_plugins[ $basename ];
7323 $site_active_plugins_cache->plugins[ $basename ]['is_active'] = true;
7324 } else if ( isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
7325 ! isset( $site_active_plugins[ $basename ] )
7326 ) {
7327 // Plugin was site level deactivated.
7328 unset( $site_active_plugins_cache->plugins[ $basename ] );
7329 }
7330
7331 $prev_version = $data['version'];
7332 $current_version = $network_plugins[ $basename ]['Version'];
7333
7334 if ( $was_active !== $is_active || $prev_version !== $current_version ) {
7335 // Plugin activated or deactivated, or version changed.
7336
7337 if ( $was_active !== $is_active ) {
7338 if ( $data['is_active'] != $network_plugins[ $basename ]['is_active'] ) {
7339 $network_plugins_cache->plugins[ $basename ]['is_active'] = $data['is_active'];
7340 }
7341 }
7342
7343 if ( $prev_version !== $current_version ) {
7344 $network_plugins_cache->plugins[ $basename ]['Version'] = $current_version;
7345 }
7346
7347 $updated_plugin_data = $data;
7348 $updated_plugin_data['is_active'] = $is_active;
7349 $updated_plugin_data['version'] = $current_version;
7350 $updated_plugin_data['title'] = $network_plugins[ $basename ]['Name'];
7351 $plugins_update_data[] = $updated_plugin_data;
7352 }
7353 }
7354
7355 // Find new plugins that weren't yet seen before.
7356 foreach ( $network_plugins as $basename => $data ) {
7357 if ( ! isset( $network_plugins_cache->plugins[ $basename ] ) ) {
7358 // New plugin.
7359 $new_plugin = array(
7360 'slug' => $data['slug'],
7361 'version' => $data['Version'],
7362 'title' => $data['Name'],
7363 'is_active' => $data['is_active'],
7364 'is_uninstalled' => false,
7365 );
7366
7367 $plugins_update_data[] = $new_plugin;
7368 $network_plugins_cache->plugins[ $basename ] = $new_plugin;
7369
7370 if ( isset( $site_active_plugins[ $basename ] ) ) {
7371 $site_active_plugins_cache->plugins[ $basename ] = $new_plugin;
7372 $site_active_plugins_cache->plugins[ $basename ]['is_active'] = true;
7373 }
7374 }
7375 }
7376
7377 $site_active_plugins_cache->md5 = $site_active_plugins_thumbprint;
7378 $site_active_plugins_cache->timestamp = $time;
7379 self::$_accounts->set_option( $site_active_plugins_option_name, $site_active_plugins_cache, true );
7380
7381 $network_plugins_cache->md5 = $network_plugins_thumbprint;
7382 $network_plugins_cache->timestamp = $time;
7383 self::$_accounts->set_option( $network_plugins_option_name, $network_plugins_cache, true );
7384
7385 return $plugins_update_data;
7386 }
7387
7388 /**
7389 * Return a list of modified themes since the last sync.
7390 *
7391 * Note:
7392 * There's no point to store a themes counter since even if the number of
7393 * themes didn't change, we still need to check if the versions are all the
7394 * same and the activity state is similar.
7395 *
7396 * @author Vova Feldman (@svovaf)
7397 * @since 1.1.8
7398 *
7399 * @return array|false
7400 */
7401 private function get_themes_data_for_api() {
7402 // Alias.
7403 $option_name = 'all_themes';
7404
7405 $all_cached_themes = self::$_accounts->get_option( $option_name );
7406
7407 if ( ! is_object( $all_cached_themes ) ) {
7408 $all_cached_themes = (object) array(
7409 'timestamp' => '',
7410 'md5' => '',
7411 'themes' => array(),
7412 );
7413 }
7414
7415 $time = time();
7416
7417 if ( ! empty( $all_cached_themes->timestamp ) &&
7418 ( $time - $all_cached_themes->timestamp ) < WP_FS__TIME_5_MIN_IN_SEC
7419 ) {
7420 // Don't send theme updates if last update was in the past 5 min.
7421 return false;
7422 }
7423
7424 // Write timestamp to lock the logic.
7425 $all_cached_themes->timestamp = $time;
7426 self::$_accounts->set_option( $option_name, $all_cached_themes, true );
7427
7428 // Reload options from DB.
7429 self::$_accounts->load( true );
7430 $all_cached_themes = self::$_accounts->get_option( $option_name );
7431
7432 if ( $time != $all_cached_themes->timestamp ) {
7433 // If timestamp is different, then another thread captured the lock.
7434 return false;
7435 }
7436
7437 // Get active theme.
7438 $active_theme = wp_get_theme();
7439 $active_theme_stylesheet = $active_theme->get_stylesheet();
7440
7441 // Check if there's a change in themes.
7442 $all_themes = wp_get_themes();
7443
7444 // Check if themes changed.
7445 ksort( $all_themes );
7446
7447 $themes_signature = '';
7448 foreach ( $all_themes as $slug => $data ) {
7449 $is_active = ( $slug === $active_theme_stylesheet );
7450 $themes_signature .= $slug . ',' .
7451 $data->version . ',' .
7452 ( $is_active ? '1' : '0' ) . ';';
7453 }
7454
7455 // Check if themes status changed (version or active/inactive).
7456 $themes_changed = ( $all_cached_themes->md5 !== md5( $themes_signature ) );
7457
7458 $themes_update_data = array();
7459
7460 if ( $themes_changed ) {
7461 // Change in themes, report changes.
7462
7463 // Update existing themes info.
7464 foreach ( $all_cached_themes->themes as $slug => $data ) {
7465 $is_active = ( $slug === $active_theme_stylesheet );
7466
7467 if ( ! isset( $all_themes[ $slug ] ) ) {
7468 // Plugin uninstalled.
7469 $uninstalled_theme_data = $data;
7470 $uninstalled_theme_data['is_active'] = false;
7471 $uninstalled_theme_data['is_uninstalled'] = true;
7472 $themes_update_data[] = $uninstalled_theme_data;
7473
7474 unset( $all_themes[ $slug ] );
7475 unset( $all_cached_themes->themes[ $slug ] );
7476 } else if ( $data['is_active'] !== $is_active ||
7477 $data['version'] !== $all_themes[ $slug ]->version
7478 ) {
7479 // Plugin activated or deactivated, or version changed.
7480
7481 $all_cached_themes->themes[ $slug ]['is_active'] = $is_active;
7482 $all_cached_themes->themes[ $slug ]['version'] = $all_themes[ $slug ]->version;
7483
7484 $themes_update_data[] = $all_cached_themes->themes[ $slug ];
7485 }
7486 }
7487
7488 // Find new themes that weren't yet seen before.
7489 foreach ( $all_themes as $slug => $data ) {
7490 if ( ! isset( $all_cached_themes->themes[ $slug ] ) ) {
7491 $is_active = ( $slug === $active_theme_stylesheet );
7492
7493 // New plugin.
7494 $new_plugin = array(
7495 'slug' => $slug,
7496 'version' => $data->version,
7497 'title' => $data->name,
7498 'is_active' => $is_active,
7499 'is_uninstalled' => false,
7500 );
7501
7502 $themes_update_data[] = $new_plugin;
7503 $all_cached_themes->themes[ $slug ] = $new_plugin;
7504 }
7505 }
7506
7507 $all_cached_themes->md5 = md5( $themes_signature );
7508 $all_cached_themes->timestamp = time();
7509 self::$_accounts->set_option( $option_name, $all_cached_themes, true );
7510 }
7511
7512 return $themes_update_data;
7513 }
7514
7515 /**
7516 * Get site data for API install request.
7517 *
7518 * @author Vova Feldman (@svovaf)
7519 * @since 1.1.2
7520 *
7521 * @param string[] string $override
7522 * @param bool $include_plugins Since 1.1.8 by default include plugin changes.
7523 * @param bool $include_themes Since 1.1.8 by default include plugin changes.
7524 *
7525 * @return array
7526 */
7527 private function get_install_data_for_api(
7528 array $override,
7529 $include_plugins = true,
7530 $include_themes = true
7531 ) {
7532 /**
7533 * @since 1.1.8 Also send plugin updates.
7534 */
7535 if ( $include_plugins && ! isset( $override['plugins'] ) ) {
7536 $plugins = $this->get_plugins_data_for_api();
7537 if ( ! empty( $plugins ) ) {
7538 $override['plugins'] = $plugins;
7539 }
7540 }
7541 /**
7542 * @since 1.1.8 Also send themes updates.
7543 */
7544 if ( $include_themes && ! isset( $override['themes'] ) ) {
7545 $themes = $this->get_themes_data_for_api();
7546 if ( ! empty( $themes ) ) {
7547 $override['themes'] = $themes;
7548 }
7549 }
7550
7551 return array_merge( array(
7552 'version' => $this->get_plugin_version(),
7553 'is_premium' => $this->is_premium(),
7554 'language' => get_bloginfo( 'language' ),
7555 'charset' => get_bloginfo( 'charset' ),
7556 'platform_version' => get_bloginfo( 'version' ),
7557 'sdk_version' => $this->version,
7558 'programming_language_version' => phpversion(),
7559 'title' => get_bloginfo( 'name' ),
7560 'url' => get_site_url(),
7561 // Special params.
7562 'is_active' => true,
7563 'is_disconnected' => $this->is_tracking_prohibited(),
7564 'is_uninstalled' => false,
7565 ), $override );
7566 }
7567
7568 /**
7569 * Update installs details.
7570 *
7571 * @todo V1 of multiste network support doesn't support plugin and theme data sending.
7572 *
7573 * @author Vova Feldman (@svovaf)
7574 * @since 2.0.0
7575 *
7576 * @param string[] string $override
7577 * @param bool $only_diff
7578 * @param bool $include_plugins Since 1.1.8 by default include plugin changes.
7579 * @param bool $include_themes Since 1.1.8 by default include plugin changes.
7580 *
7581 * @return array
7582 */
7583 private function get_installs_data_for_api(
7584 array $override,
7585 $only_diff = false,
7586 $include_plugins = true,
7587 $include_themes = true
7588 ) {
7589 /**
7590 * @since 1.1.8 Also send plugin updates.
7591 */
7592 // if ( $include_plugins && ! isset( $override['plugins'] ) ) {
7593 // $plugins = $this->get_plugins_data_for_api();
7594 // if ( ! empty( $plugins ) ) {
7595 // $override['plugins'] = $plugins;
7596 // }
7597 // }
7598 /**
7599 * @since 1.1.8 Also send themes updates.
7600 */
7601 // if ( $include_themes && ! isset( $override['themes'] ) ) {
7602 // $themes = $this->get_themes_data_for_api();
7603 // if ( ! empty( $themes ) ) {
7604 // $override['themes'] = $themes;
7605 // }
7606 // }
7607
7608 // Common properties.
7609 $common = array_merge( array(
7610 'version' => $this->get_plugin_version(),
7611 'is_premium' => $this->is_premium(),
7612 'sdk_version' => $this->version,
7613 'programming_language_version' => phpversion(),
7614 'platform_version' => get_bloginfo( 'version' ),
7615 ), $override );
7616
7617
7618 $is_common_diff_for_any_site = false;
7619 $common_diff_union = array();
7620
7621 $installs_data = array();
7622
7623 $sites = self::get_sites();
7624
7625 foreach ( $sites as $site ) {
7626 $blog_id = self::get_site_blog_id( $site );
7627
7628 $install = $this->get_install_by_blog_id( $blog_id );
7629
7630 if ( is_object( $install ) ) {
7631 if ( $install->user_id != $this->_user->id ) {
7632 // Install belongs to a different owner.
7633 continue;
7634 }
7635
7636 if ( ! $this->is_premium() && $install->is_tracking_prohibited() ) {
7637 // Don't send updates regarding opted-out installs.
7638 continue;
7639 }
7640
7641 $install_data = $this->get_site_info( $site );
7642
7643 $uid = $install_data['uid'];
7644
7645 unset( $install_data['blog_id'] );
7646 unset( $install_data['uid'] );
7647
7648 $install_data['is_disconnected'] = $install->is_disconnected;
7649 $install_data['is_active'] = $this->is_active_for_site( $blog_id );
7650 $install_data['is_uninstalled'] = $install->is_uninstalled;
7651
7652 $common_diff = null;
7653 $is_common_diff = false;
7654 if ( $only_diff ) {
7655 $install_data = $this->get_install_diff_for_api( $install_data, $install, $override );
7656 $common_diff = $this->get_install_diff_for_api( $common, $install, $override );
7657
7658 $is_common_diff = ! empty( $common_diff );
7659
7660 if ( $is_common_diff ) {
7661 foreach ( $common_diff as $k => $v ) {
7662 if ( ! isset( $common_diff_union[ $k ] ) ) {
7663 $common_diff_union[ $k ] = $v;
7664 }
7665 }
7666 }
7667
7668 $is_common_diff_for_any_site = $is_common_diff_for_any_site || $is_common_diff;
7669 }
7670
7671 if ( ! empty( $install_data ) || $is_common_diff ) {
7672 // Add install ID and site unique ID.
7673 $install_data['id'] = $install->id;
7674 $install_data['uid'] = $uid;
7675
7676 $installs_data[] = $install_data;
7677 }
7678 }
7679 }
7680
7681 restore_current_blog();
7682
7683 if ( 0 < count( $installs_data ) && ( $is_common_diff_for_any_site || ! $only_diff ) ) {
7684 if ( ! $only_diff ) {
7685 $installs_data[] = $common;
7686 } else if ( ! empty( $common_diff_union ) ) {
7687 $installs_data[] = $common_diff_union;
7688 }
7689 }
7690
7691 foreach ( $installs_data as &$data ) {
7692 $data = (object) $data;
7693 }
7694
7695 return $installs_data;
7696 }
7697
7698 /**
7699 * Compare site actual data to the stored install data and return the differences for an API data sync.
7700 *
7701 * @author Vova Feldman (@svovaf)
7702 * @since 2.0.0
7703 *
7704 * @param array $site
7705 * @param FS_Site $install
7706 * @param string[] string $override
7707 *
7708 * @return array
7709 */
7710 private function get_install_diff_for_api( $site, $install, $override = array() ) {
7711 $diff = array();
7712 $special = array();
7713 $special_override = false;
7714
7715 foreach ( $site as $p => $v ) {
7716 if ( property_exists( $install, $p ) ) {
7717 if ( ( is_bool( $install->{$p} ) || ! empty( $install->{$p} ) ) &&
7718 $install->{$p} != $v
7719 ) {
7720 $install->{$p} = $v;
7721 $diff[ $p ] = $v;
7722 }
7723 } else {
7724 $special[ $p ] = $v;
7725
7726 if ( isset( $override[ $p ] ) ||
7727 'plugins' === $p ||
7728 'themes' === $p
7729 ) {
7730 $special_override = true;
7731 }
7732 }
7733 }
7734
7735 if ( $special_override || 0 < count( $diff ) ) {
7736 // Add special params only if has at least one
7737 // standard param, or if explicitly requested to
7738 // override a special param or a param which is not exist
7739 // in the install object.
7740 $diff = array_merge( $diff, $special );
7741 }
7742
7743 return $diff;
7744 }
7745
7746 /**
7747 * Update install only if changed.
7748 *
7749 * @author Vova Feldman (@svovaf)
7750 * @since 1.0.9
7751 *
7752 * @param string[] string $override
7753 * @param bool $flush
7754 *
7755 * @return false|object|string
7756 */
7757 private function send_install_update( $override = array(), $flush = false ) {
7758 $this->_logger->entrance();
7759
7760 $check_properties = $this->get_install_data_for_api( $override );
7761
7762 if ( $flush ) {
7763 $params = $check_properties;
7764 } else {
7765 $params = $this->get_install_diff_for_api( $check_properties, $this->_site, $override );
7766 }
7767
7768 if ( 0 < count( $params ) ) {
7769 if ( ! is_multisite() ) {
7770 // Update last install sync timestamp.
7771 $this->set_cron_execution_timestamp( 'install_sync' );
7772 }
7773
7774 $params['uid'] = $this->get_anonymous_id();
7775
7776 // Send updated values to FS.
7777 $site = $this->get_api_site_scope()->call( '/', 'put', $params );
7778
7779 if ( $this->is_api_result_entity( $site ) ) {
7780 if ( ! is_multisite() ) {
7781 // I successfully sent install update, clear scheduled sync if exist.
7782 $this->clear_install_sync_cron();
7783 }
7784 }
7785
7786 return $site;
7787 }
7788
7789 return false;
7790 }
7791
7792 /**
7793 * Update installs only if changed.
7794 *
7795 * @author Vova Feldman (@svovaf)
7796 * @since 2.0.0
7797 *
7798 * @param string[] string $override
7799 * @param bool $flush
7800 *
7801 * @return false|object|string
7802 */
7803 private function send_installs_update( $override = array(), $flush = false ) {
7804 $this->_logger->entrance();
7805
7806 $installs_data = $this->get_installs_data_for_api( $override, ! $flush );
7807
7808 if ( empty( $installs_data ) ) {
7809 return false;
7810 }
7811
7812 // Update last install sync timestamp.
7813 $this->set_cron_execution_timestamp( 'install_sync' );
7814
7815 // Send updated values to FS.
7816 $result = $this->get_api_user_scope()->call( "/plugins/{$this->_plugin->id}/installs.json", 'put', $installs_data );
7817
7818 if ( $this->is_api_result_object( $result, 'installs' ) ) {
7819 // I successfully sent installs update, clear scheduled sync if exist.
7820 $this->clear_install_sync_cron();
7821 }
7822
7823 return $result;
7824 }
7825
7826 /**
7827 * Update install only if changed.
7828 *
7829 * @author Vova Feldman (@svovaf)
7830 * @since 1.0.9
7831 *
7832 * @param string[] string $override
7833 * @param bool $flush
7834 */
7835 private function sync_install( $override = array(), $flush = false ) {
7836 $this->_logger->entrance();
7837
7838 $site = $this->send_install_update( $override, $flush );
7839
7840 if ( false === $site ) {
7841 // No sync required.
7842 return;
7843 }
7844
7845 if ( ! $this->is_api_result_entity( $site ) ) {
7846 // Failed to sync, don't update locally.
7847 return;
7848 }
7849
7850 $this->_site = new FS_Site( $site );
7851
7852 $this->_store_site( true );
7853 }
7854
7855 /**
7856 * Update install only if changed.
7857 *
7858 * @author Vova Feldman (@svovaf)
7859 * @since 1.0.9
7860 *
7861 * @param string[] string $override
7862 * @param bool $flush
7863 */
7864 private function sync_installs( $override = array(), $flush = false ) {
7865 $this->_logger->entrance();
7866
7867 $result = $this->send_installs_update( $override, $flush );
7868
7869 if ( false === $result ) {
7870 // No sync required.
7871 return;
7872 }
7873
7874 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
7875 // Failed to sync, don't update locally.
7876 return;
7877 }
7878
7879 $address_to_blog_map = $this->get_address_to_blog_map();
7880
7881 foreach ( $result->installs as $install ) {
7882 $this->_site = new FS_Site( $install );
7883
7884 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
7885 $blog_id = $address_to_blog_map[ $address ];
7886
7887 $this->_store_site( true, $blog_id );
7888 }
7889 }
7890
7891 /**
7892 * Track install's custom event.
7893 *
7894 * IMPORTANT:
7895 * Custom event tracking is currently only supported for specific clients.
7896 * If you are not one of them, please don't use this method. If you will,
7897 * the API will simply ignore your request based on the plugin ID.
7898 *
7899 * Need custom tracking for your plugin or theme?
7900 * If you are interested in custom event tracking please contact yo@freemius.com
7901 * for further details.
7902 *
7903 * @author Vova Feldman (@svovaf)
7904 * @since 1.2.1
7905 *
7906 * @param string $name Event name.
7907 * @param array $properties Associative key/value array with primitive values only
7908 * @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
7909 * @param bool $once If true, event will be tracked only once. IMPORTANT: Still trigger the API call.
7910 *
7911 * @return object|false Event data or FALSE on failure.
7912 *
7913 * @throws \Freemius_InvalidArgumentException
7914 */
7915 public function track_event( $name, $properties = array(), $process_at = false, $once = false ) {
7916 $this->_logger->entrance( http_build_query( array( 'name' => $name, 'once' => $once ) ) );
7917
7918 if ( ! $this->is_registered() ) {
7919 return false;
7920 }
7921
7922 $event = array( 'type' => $name );
7923
7924 if ( is_numeric( $process_at ) && $process_at > time() ) {
7925 $event['process_at'] = $process_at;
7926 }
7927
7928 if ( $once ) {
7929 $event['once'] = true;
7930 }
7931
7932 if ( ! empty( $properties ) ) {
7933 // Verify associative array values are primitive.
7934 foreach ( $properties as $k => $v ) {
7935 if ( ! is_scalar( $v ) ) {
7936 throw new Freemius_InvalidArgumentException( 'The $properties argument must be an associative key/value array with primitive values only.' );
7937 }
7938 }
7939
7940 $event['properties'] = $properties;
7941 }
7942
7943 $result = $this->get_api_site_scope()->call( 'events.json', 'post', $event );
7944
7945 return $this->is_api_error( $result ) ?
7946 false :
7947 $result;
7948 }
7949
7950 /**
7951 * Track install's custom event only once, but it still triggers the API call.
7952 *
7953 * IMPORTANT:
7954 * Custom event tracking is currently only supported for specific clients.
7955 * If you are not one of them, please don't use this method. If you will,
7956 * the API will simply ignore your request based on the plugin ID.
7957 *
7958 * Need custom tracking for your plugin or theme?
7959 * If you are interested in custom event tracking please contact yo@freemius.com
7960 * for further details.
7961 *
7962 * @author Vova Feldman (@svovaf)
7963 * @since 1.2.1
7964 *
7965 * @param string $name Event name.
7966 * @param array $properties Associative key/value array with primitive values only
7967 * @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
7968 *
7969 * @return object|false Event data or FALSE on failure.
7970 *
7971 * @throws \Freemius_InvalidArgumentException
7972 *
7973 * @user Freemius::track_event()
7974 */
7975 public function track_event_once( $name, $properties = array(), $process_at = false ) {
7976 return $this->track_event( $name, $properties, $process_at, true );
7977 }
7978
7979 /**
7980 * Plugin uninstall hook.
7981 *
7982 * @author Vova Feldman (@svovaf)
7983 * @since 1.0.1
7984 *
7985 * @param bool $check_user Enforce checking if user have plugins activation privileges.
7986 */
7987 function _uninstall_plugin_event( $check_user = true ) {
7988 $this->_logger->entrance( 'slug = ' . $this->_slug );
7989
7990 if ( $check_user && ! current_user_can( 'activate_plugins' ) ) {
7991 return;
7992 }
7993
7994 $params = array();
7995 $uninstall_reason = null;
7996 if ( isset( $this->_storage->uninstall_reason ) ) {
7997 $uninstall_reason = $this->_storage->uninstall_reason;
7998 $params['reason_id'] = $uninstall_reason->id;
7999 $params['reason_info'] = $uninstall_reason->info;
8000 }
8001
8002 if ( ! $this->is_registered() ) {
8003 // Send anonymous uninstall event only if user submitted a feedback.
8004 if ( isset( $uninstall_reason ) ) {
8005 if ( isset( $uninstall_reason->is_anonymous ) && ! $uninstall_reason->is_anonymous ) {
8006 $this->opt_in( false, false, false, false, true );
8007 } else {
8008 $params['uid'] = $this->get_anonymous_id();
8009 $this->get_api_plugin_scope()->call( 'uninstall.json', 'put', $params );
8010 }
8011 }
8012 } else {
8013 $params = array_merge( $params, array(
8014 'is_active' => false,
8015 'is_uninstalled' => true,
8016 ) );
8017
8018 if ( $this->_is_network_active ) {
8019 // Send uninstall event.
8020 $this->send_installs_update( $params );
8021 } else {
8022 // Send uninstall event.
8023 $this->send_install_update( $params );
8024 }
8025 }
8026
8027 // @todo Decide if we want to delete plugin information from db.
8028 }
8029
8030 /**
8031 * @author Vova Feldman (@svovaf)
8032 * @since 1.1.1
8033 *
8034 * @return string
8035 */
8036 function premium_plugin_basename() {
8037 return "{$this->_slug}-premium/" . basename( $this->_free_plugin_basename );
8038 }
8039
8040 /**
8041 * Uninstall plugin hook. Called only when connected his account with Freemius for active sites tracking.
8042 *
8043 * @author Vova Feldman (@svovaf)
8044 * @since 1.0.2
8045 */
8046 public static function _uninstall_plugin_hook() {
8047 self::_load_required_static();
8048
8049 self::$_static_logger->entrance();
8050
8051 if ( ! current_user_can( 'activate_plugins' ) ) {
8052 return;
8053 }
8054
8055 $plugin_file = substr( current_filter(), strlen( 'uninstall_' ) );
8056
8057 self::$_static_logger->info( 'plugin = ' . $plugin_file );
8058
8059 define( 'WP_FS__UNINSTALL_MODE', true );
8060
8061 $fs = self::get_instance_by_file( $plugin_file );
8062
8063 if ( is_object( $fs ) ) {
8064 self::require_plugin_essentials();
8065
8066 if ( is_plugin_active( $fs->_free_plugin_basename ) ||
8067 is_plugin_active( $fs->premium_plugin_basename() )
8068 ) {
8069 // Deleting Free or Premium plugin version while the other version still installed.
8070 return;
8071 }
8072
8073 $fs->_uninstall_plugin_event();
8074
8075 $fs->do_action( 'after_uninstall' );
8076 }
8077 }
8078
8079 #----------------------------------------------------------------------------------
8080 #region Plugin Information
8081 #----------------------------------------------------------------------------------
8082
8083 /**
8084 * Load WordPress core plugin.php essential module.
8085 *
8086 * @author Vova Feldman (@svovaf)
8087 * @since 1.1.1
8088 */
8089 private static function require_plugin_essentials() {
8090 if ( ! function_exists( 'get_plugins' ) ) {
8091 self::$_static_logger->log( 'Including wp-admin/includes/plugin.php...' );
8092
8093 require_once ABSPATH . 'wp-admin/includes/plugin.php';
8094 }
8095 }
8096
8097 /**
8098 * Load WordPress core pluggable.php module.
8099 *
8100 * @author Vova Feldman (@svovaf)
8101 * @since 1.1.2
8102 */
8103 private static function require_pluggable_essentials() {
8104 if ( ! function_exists( 'wp_get_current_user' ) ) {
8105 require_once ABSPATH . 'wp-includes/pluggable.php';
8106 }
8107 }
8108
8109 /**
8110 * Return plugin data.
8111 *
8112 * @author Vova Feldman (@svovaf)
8113 * @since 1.0.1
8114 *
8115 * @return array
8116 */
8117 function get_plugin_data() {
8118 if ( ! isset( $this->_plugin_data ) ) {
8119 self::require_plugin_essentials();
8120
8121 if ( $this->is_plugin() ) {
8122 /**
8123 * @author Vova Feldman (@svovaf)
8124 * @since 1.2.0 When using get_plugin_data() do NOT translate plugin data.
8125 *
8126 * @link https://github.com/Freemius/wordpress-sdk/issues/77
8127 */
8128 $plugin_data = get_plugin_data(
8129 $this->_plugin_main_file_path,
8130 false,
8131 false
8132 );
8133 } else {
8134 $theme_data = wp_get_theme();
8135
8136 $plugin_data = array(
8137 'Name' => $theme_data->get( 'Name' ),
8138 'Version' => $theme_data->get( 'Version' ),
8139 'Author' => $theme_data->get( 'Author' ),
8140 'Description' => $theme_data->get( 'Description' ),
8141 'PluginURI' => $theme_data->get( 'ThemeURI' ),
8142 );
8143 }
8144
8145 $this->_plugin_data = $plugin_data;
8146 }
8147
8148 return $this->_plugin_data;
8149 }
8150
8151 /**
8152 * @author Vova Feldman (@svovaf)
8153 * @since 1.0.1
8154 * @since 1.2.2.5 If slug not set load slug by module ID.
8155 *
8156 * @return string Plugin slug.
8157 */
8158 function get_slug() {
8159 if ( ! isset( $this->_slug ) ) {
8160 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
8161 $this->_slug = $id_slug_type_path_map[ $this->_module_id ]['slug'];
8162 }
8163
8164 return $this->_slug;
8165 }
8166
8167 /**
8168 * @author Vova Feldman (@svovaf)
8169 * @since 1.2.1.7
8170 *
8171 * @return string Plugin slug.
8172 */
8173 function get_target_folder_name() {
8174 return $this->_slug . ( $this->can_use_premium_code() ? '-premium' : '' );
8175 }
8176
8177 /**
8178 * @author Vova Feldman (@svovaf)
8179 * @since 1.0.1
8180 *
8181 * @return number Plugin ID.
8182 */
8183 function get_id() {
8184 return $this->_plugin->id;
8185 }
8186
8187 /**
8188 * @author Vova Feldman (@svovaf)
8189 * @since 1.2.1.5
8190 *
8191 * @return string Freemius SDK version
8192 */
8193 function get_sdk_version() {
8194 return $this->version;
8195 }
8196
8197 /**
8198 * @author Vova Feldman (@svovaf)
8199 * @since 1.2.1.5
8200 *
8201 * @return number Parent plugin ID (if parent exist).
8202 */
8203 function get_parent_id() {
8204 return $this->is_addon() ?
8205 $this->get_parent_instance()->get_id() :
8206 $this->_plugin->id;
8207 }
8208
8209 /**
8210 * @author Vova Feldman (@svovaf)
8211 * @since 1.0.1
8212 *
8213 * @return string Plugin public key.
8214 */
8215 function get_public_key() {
8216 return $this->_plugin->public_key;
8217 }
8218
8219 /**
8220 * Will be available only on sandbox mode.
8221 *
8222 * @author Vova Feldman (@svovaf)
8223 * @since 1.0.4
8224 *
8225 * @return mixed Plugin secret key.
8226 */
8227 function get_secret_key() {
8228 return $this->_plugin->secret_key;
8229 }
8230
8231 /**
8232 * @author Vova Feldman (@svovaf)
8233 * @since 1.1.1
8234 *
8235 * @return bool
8236 */
8237 function has_secret_key() {
8238 return ! empty( $this->_plugin->secret_key );
8239 }
8240
8241 /**
8242 * @author Vova Feldman (@svovaf)
8243 * @since 1.0.9
8244 *
8245 * @return string
8246 */
8247 function get_plugin_name() {
8248 $this->_logger->entrance();
8249
8250 if ( ! isset( $this->_plugin_name ) ) {
8251 $plugin_data = $this->get_plugin_data();
8252
8253 // Get name.
8254 $this->_plugin_name = $plugin_data['Name'];
8255
8256 // Check if plugin name contains "(Premium)" suffix and remove it.
8257 $suffix = ' (premium)';
8258 $suffix_len = strlen( $suffix );
8259
8260 if ( strlen( $plugin_data['Name'] ) > $suffix_len &&
8261 $suffix === substr( strtolower( $plugin_data['Name'] ), - $suffix_len )
8262 ) {
8263 $this->_plugin_name = substr( $plugin_data['Name'], 0, - $suffix_len );
8264 }
8265
8266 $this->_logger->departure( 'Name = ' . $this->_plugin_name );
8267 }
8268
8269 return $this->_plugin_name;
8270 }
8271
8272 /**
8273 * @author Vova Feldman (@svovaf)
8274 * @since 1.0.0
8275 *
8276 * @return string
8277 */
8278 function get_plugin_version() {
8279 $this->_logger->entrance();
8280
8281 $plugin_data = $this->get_plugin_data();
8282
8283 $this->_logger->departure( 'Version = ' . $plugin_data['Version'] );
8284
8285 return $this->apply_filters( 'plugin_version', $plugin_data['Version'] );
8286 }
8287
8288 /**
8289 * @author Vova Feldman (@svovaf)
8290 * @since 1.2.1.7
8291 *
8292 * @return string
8293 */
8294 function get_plugin_title() {
8295 $this->_logger->entrance();
8296
8297 $title = $this->_plugin->title;
8298
8299 return $this->apply_filters( 'plugin_title', $title );
8300 }
8301
8302 /**
8303 * @author Vova Feldman (@svovaf)
8304 * @since 1.2.2.7
8305 *
8306 * @param bool $lowercase
8307 *
8308 * @return string
8309 */
8310 function get_module_label( $lowercase = false ) {
8311 $label = $this->is_addon() ?
8312 $this->get_text_inline( 'Add-On', 'addon' ) :
8313 ( $this->is_plugin() ?
8314 $this->get_text_inline( 'Plugin', 'plugin' ) :
8315 $this->get_text_inline( 'Theme', 'theme' ) );
8316
8317 if ( $lowercase ) {
8318 $label = strtolower( $label );
8319 }
8320
8321 return $label;
8322 }
8323
8324 /**
8325 * @author Vova Feldman (@svovaf)
8326 * @since 1.0.4
8327 *
8328 * @return string
8329 */
8330 function get_plugin_basename() {
8331 if ( ! isset( $this->_plugin_basename ) ) {
8332 if ( $this->is_plugin() ) {
8333 $this->_plugin_basename = plugin_basename( $this->_plugin_main_file_path );
8334 } else {
8335 $this->_plugin_basename = basename( dirname( $this->_plugin_main_file_path ) );
8336 }
8337 }
8338
8339 return $this->_plugin_basename;
8340 }
8341
8342 function get_plugin_folder_name() {
8343 $this->_logger->entrance();
8344
8345 $plugin_folder = $this->_plugin_basename;
8346
8347 while ( '.' !== dirname( $plugin_folder ) ) {
8348 $plugin_folder = dirname( $plugin_folder );
8349 }
8350
8351 $this->_logger->departure( 'Folder Name = ' . $plugin_folder );
8352
8353 return $plugin_folder;
8354 }
8355
8356 #endregion ------------------------------------------------------------------
8357
8358 /* Account
8359 ------------------------------------------------------------------------------------------------------------------*/
8360
8361 /**
8362 * Find plugin's slug by plugin's basename.
8363 *
8364 * @author Vova Feldman (@svovaf)
8365 * @since 1.0.9
8366 *
8367 * @param string $plugin_base_name
8368 *
8369 * @return false|string
8370 */
8371 private static function find_slug_by_basename( $plugin_base_name ) {
8372 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
8373
8374 if ( ! array( $file_slug_map ) || ! isset( $file_slug_map[ $plugin_base_name ] ) ) {
8375 return false;
8376 }
8377
8378 return $file_slug_map[ $plugin_base_name ];
8379 }
8380
8381 /**
8382 * Store the map between the plugin's basename to the slug.
8383 *
8384 * @author Vova Feldman (@svovaf)
8385 * @since 1.0.9
8386 */
8387 private function store_file_slug_map() {
8388 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
8389
8390 if ( ! array( $file_slug_map ) ) {
8391 $file_slug_map = array();
8392 }
8393
8394 if ( ! isset( $file_slug_map[ $this->_plugin_basename ] ) ||
8395 $file_slug_map[ $this->_plugin_basename ] !== $this->_slug
8396 ) {
8397 $file_slug_map[ $this->_plugin_basename ] = $this->_slug;
8398 self::$_accounts->set_option( 'file_slug_map', $file_slug_map, true );
8399 }
8400 }
8401
8402 /**
8403 * @return array[number]FS_User
8404 */
8405 static function get_all_users() {
8406 $users = self::$_accounts->get_option( 'users', array() );
8407
8408 if ( ! is_array( $users ) ) {
8409 $users = array();
8410 }
8411
8412 return $users;
8413 }
8414
8415 /**
8416 * @param string $module_type
8417 * @param null|int $blog_id Since 2.0.0
8418 *
8419 * @return array[string]FS_Site
8420 */
8421 private static function get_all_sites(
8422 $module_type = WP_FS__MODULE_TYPE_PLUGIN,
8423 $blog_id = null
8424 ) {
8425 $sites = self::get_account_option( 'sites', $module_type, $blog_id );
8426
8427 if ( ! is_array( $sites ) ) {
8428 $sites = array();
8429 }
8430
8431 return $sites;
8432 }
8433
8434 /**
8435 * @author Leo Fajardo (@leorw)
8436 *
8437 * @since 1.2.2
8438 *
8439 * @param string $option_name
8440 * @param string $module_type
8441 * @param null|int $network_level_or_blog_id Since 2.0.0
8442 *
8443 * @return mixed
8444 */
8445 private static function get_account_option( $option_name, $module_type = null, $network_level_or_blog_id = null ) {
8446 if ( ! is_null( $module_type ) && WP_FS__MODULE_TYPE_PLUGIN !== $module_type ) {
8447 $option_name = $module_type . '_' . $option_name;
8448 }
8449
8450 return self::$_accounts->get_option( $option_name, array(), $network_level_or_blog_id );
8451 }
8452
8453 /**
8454 * @author Leo Fajardo (@leorw)
8455 *
8456 * @since 1.2.2
8457 *
8458 * @param string $option_name
8459 * @param mixed $option_value
8460 * @param bool $store
8461 * @param null|int $network_level_or_blog_id Since 2.0.0
8462 */
8463 private function set_account_option( $option_name, $option_value, $store, $network_level_or_blog_id = null ) {
8464 self::set_account_option_by_module(
8465 $this->_module_type,
8466 $option_name,
8467 $option_value,
8468 $store,
8469 $network_level_or_blog_id
8470 );
8471 }
8472
8473 /**
8474 * @author Vova Feldman (@svovaf)
8475 *
8476 * @since 1.2.2.7
8477 *
8478 * @param string $module_type
8479 * @param string $option_name
8480 * @param mixed $option_value
8481 * @param bool $store
8482 * @param null|int $network_level_or_blog_id Since 2.0.0
8483 */
8484 private static function set_account_option_by_module(
8485 $module_type,
8486 $option_name,
8487 $option_value,
8488 $store,
8489 $network_level_or_blog_id = null
8490 ) {
8491 if ( WP_FS__MODULE_TYPE_PLUGIN != $module_type ) {
8492 $option_name = $module_type . '_' . $option_name;
8493 }
8494
8495 self::$_accounts->set_option( $option_name, $option_value, $store, $network_level_or_blog_id );
8496 }
8497
8498 /**
8499 * @author Vova Feldman (@svovaf)
8500 * @since 1.0.6
8501 *
8502 * @param number|null $module_id
8503 *
8504 * @return FS_Plugin_License[]
8505 */
8506 private static function get_all_licenses( $module_id = null ) {
8507 $licenses = self::get_account_option( 'all_licenses' );
8508
8509 if ( ! is_array( $licenses ) ) {
8510 $licenses = array();
8511 }
8512
8513 if ( is_null( $module_id ) ) {
8514 return $licenses;
8515 }
8516
8517 $licenses = isset( $licenses[ $module_id ] ) ?
8518 $licenses[ $module_id ] :
8519 array();
8520
8521 return $licenses;
8522 }
8523
8524 /**
8525 * @author Leo Fajardo (@leorw)
8526 * @since 2.0.0
8527 *
8528 * @return array
8529 */
8530 private static function get_all_licenses_by_module_type() {
8531 $licenses = self::get_account_option( 'all_licenses' );
8532
8533 $licenses_by_module_type = array(
8534 WP_FS__MODULE_TYPE_PLUGIN => array(),
8535 WP_FS__MODULE_TYPE_THEME => array()
8536 );
8537
8538 if ( ! is_array( $licenses ) ) {
8539 return $licenses_by_module_type;
8540 }
8541
8542 foreach ( $licenses as $module_id => $module_licenses ) {
8543 $fs = self::get_instance_by_id( $module_id );
8544 if ( false === $fs ) {
8545 continue;
8546 }
8547
8548 $licenses_by_module_type[ $fs->_module_type ] = array_merge( $licenses_by_module_type[ $fs->_module_type ], $module_licenses );
8549 }
8550
8551 return $licenses_by_module_type;
8552 }
8553
8554 /**
8555 * @author Leo Fajardo (@leorw)
8556 * @since 2.0.0
8557 *
8558 * @param number $module_id
8559 * @param number|null $user_id
8560 *
8561 * @return array
8562 */
8563 private static function get_user_id_license_ids_map( $module_id, $user_id = null ) {
8564 $all_modules_user_id_license_ids_map = self::get_account_option( 'user_id_license_ids_map' );
8565
8566 if ( ! is_array( $all_modules_user_id_license_ids_map ) ) {
8567 $all_modules_user_id_license_ids_map = array();
8568 }
8569
8570 $user_id_license_ids_map = isset( $all_modules_user_id_license_ids_map[ $module_id ] ) ?
8571 $all_modules_user_id_license_ids_map[ $module_id ] :
8572 array();
8573
8574 if ( FS_User::is_valid_id( $user_id ) ) {
8575 $user_id_license_ids_map = isset( $user_id_license_ids_map[ $user_id ] ) ?
8576 $user_id_license_ids_map[ $user_id ] :
8577 array();
8578 }
8579
8580 return $user_id_license_ids_map;
8581 }
8582
8583 /**
8584 * @author Leo Fajardo (@leorw)
8585 * @since 2.0.0
8586 *
8587 * @param array $new_user_id_license_ids_map
8588 * @param number $module_id
8589 * @param number|null $user_id
8590 */
8591 private static function store_user_id_license_ids_map( $new_user_id_license_ids_map, $module_id, $user_id = null ) {
8592 $all_modules_user_id_license_ids_map = self::get_account_option( 'user_id_license_ids_map' );
8593 if ( ! is_array( $all_modules_user_id_license_ids_map ) ) {
8594 $all_modules_user_id_license_ids_map = array();
8595 }
8596
8597 if ( ! isset( $all_modules_user_id_license_ids_map[ $module_id ] ) ) {
8598 $all_modules_user_id_license_ids_map[ $module_id ] = array();
8599 }
8600
8601 if ( FS_User::is_valid_id( $user_id ) ) {
8602 $all_modules_user_id_license_ids_map[ $module_id ][ $user_id ] = $new_user_id_license_ids_map;
8603 } else {
8604 $all_modules_user_id_license_ids_map[ $module_id ] = $new_user_id_license_ids_map;
8605 }
8606
8607 self::$_accounts->set_option( 'user_id_license_ids_map', $all_modules_user_id_license_ids_map, true );
8608 }
8609
8610 /**
8611 * Get a collection of the user's linked license IDs.
8612 *
8613 * @author Vova Feldman (@svovaf)
8614 * @since 2.0.0
8615 *
8616 * @param number $user_id
8617 *
8618 * @return number[]
8619 */
8620 private function get_user_linked_license_ids( $user_id ) {
8621 return self::get_user_id_license_ids_map( $this->_module_id, $user_id );
8622 }
8623
8624 /**
8625 * Override the user's linked license IDs with a new IDs collection.
8626 *
8627 * @author Vova Feldman (@svovaf)
8628 * @since 2.0.0
8629 *
8630 * @param number $user_id
8631 * @param number[] $license_ids
8632 */
8633 private function set_user_linked_license_ids( $user_id, array $license_ids ) {
8634 self::store_user_id_license_ids_map( $license_ids, $this->_module_id, $user_id );
8635 }
8636
8637 /**
8638 * Link a specified license ID to a given user.
8639 *
8640 * @author Vova Feldman (@svovaf)
8641 * @since 2.0.0
8642 *
8643 * @param number $license_id
8644 * @param number $user_id
8645 */
8646 private function link_license_2_user( $license_id, $user_id ) {
8647 $license_ids = $this->get_user_linked_license_ids( $user_id );
8648
8649 if ( in_array( $license_id, $license_ids ) ) {
8650 // License already linked.
8651 return;
8652 }
8653
8654 $license_ids[] = $license_id;
8655
8656 $this->set_user_linked_license_ids( $user_id, $license_ids );
8657 }
8658
8659 /**
8660 * @param string|bool $module_type
8661 *
8662 * @return FS_Plugin_Plan[]
8663 */
8664 private static function get_all_plans( $module_type = false ) {
8665 $plans = self::get_account_option( 'plans', $module_type );
8666
8667 if ( ! is_array( $plans ) ) {
8668 $plans = array();
8669 }
8670
8671 return $plans;
8672 }
8673
8674 /**
8675 * @author Vova Feldman (@svovaf)
8676 * @since 1.0.4
8677 *
8678 * @return FS_Plugin_Tag[]
8679 */
8680 private static function get_all_updates() {
8681 $updates = self::$_accounts->get_option( 'updates', array() );
8682
8683 if ( ! is_array( $updates ) ) {
8684 $updates = array();
8685 }
8686
8687 return $updates;
8688 }
8689
8690 /**
8691 * @author Vova Feldman (@svovaf)
8692 * @since 1.0.6
8693 *
8694 * @return array<number,FS_Plugin[]>|false
8695 */
8696 private static function get_all_addons() {
8697 $addons = self::$_accounts->get_option( 'addons', array() );
8698
8699 if ( ! is_array( $addons ) ) {
8700 $addons = array();
8701 }
8702
8703 return $addons;
8704 }
8705
8706 /**
8707 * @author Vova Feldman (@svovaf)
8708 * @since 1.0.6
8709 *
8710 * @return FS_Plugin[]|false
8711 */
8712 private static function get_all_account_addons() {
8713 $addons = self::$_accounts->get_option( 'account_addons', array() );
8714
8715 if ( ! is_array( $addons ) ) {
8716 $addons = array();
8717 }
8718
8719 return $addons;
8720 }
8721
8722 /**
8723 * Check if user has connected his account (opted-in).
8724 *
8725 * Note:
8726 * If the user opted-in and opted-out on a later stage,
8727 * this will still return true. If you want to check if the
8728 * user is currently opted-in, use:
8729 * `$fs->is_registered() && $fs->is_tracking_allowed()`
8730 *
8731 * @author Vova Feldman (@svovaf)
8732 * @since 1.0.1
8733 * @return bool
8734 */
8735 function is_registered() {
8736 return is_object( $this->_user );
8737 }
8738
8739 /**
8740 * Returns TRUE if the user opted-in and didn't disconnect (opt-out).
8741 *
8742 * @author Leo Fajardo (@leorw)
8743 * @since 1.2.1.5
8744 *
8745 * @return bool
8746 */
8747 function is_tracking_allowed() {
8748 return ( is_object( $this->_site ) && $this->_site->is_tracking_allowed() );
8749 }
8750
8751 /**
8752 * @author Vova Feldman (@svovaf)
8753 * @since 1.0.4
8754 *
8755 * @return FS_Plugin
8756 */
8757 function get_plugin() {
8758 return $this->_plugin;
8759 }
8760
8761 /**
8762 * @author Vova Feldman (@svovaf)
8763 * @since 1.0.3
8764 *
8765 * @return FS_User
8766 */
8767 function get_user() {
8768 return $this->_user;
8769 }
8770
8771 /**
8772 * @author Vova Feldman (@svovaf)
8773 * @since 1.0.3
8774 *
8775 * @return FS_Site
8776 */
8777 function get_site() {
8778 return $this->_site;
8779 }
8780
8781 /**
8782 * Get plugin add-ons.
8783 *
8784 * @author Vova Feldman (@svovaf)
8785 * @since 1.0.6
8786 *
8787 * @since 1.1.7.3 If not yet loaded, fetch data from the API.
8788 *
8789 * @param bool $flush
8790 *
8791 * @return FS_Plugin[]|false
8792 */
8793 function get_addons( $flush = false ) {
8794 $this->_logger->entrance();
8795
8796 if ( ! $this->_has_addons ) {
8797 return false;
8798 }
8799
8800 $addons = $this->sync_addons( $flush );
8801
8802 return ( ! is_array( $addons ) || empty( $addons ) ) ?
8803 false :
8804 $addons;
8805 }
8806
8807 /**
8808 * @author Vova Feldman (@svovaf)
8809 * @since 1.0.6
8810 *
8811 * @return FS_Plugin[]|false
8812 */
8813 function get_account_addons() {
8814 $this->_logger->entrance();
8815
8816 $addons = self::get_all_account_addons();
8817
8818 if ( ! is_array( $addons ) ||
8819 ! isset( $addons[ $this->_plugin->id ] ) ||
8820 ! is_array( $addons[ $this->_plugin->id ] ) ||
8821 0 === count( $addons[ $this->_plugin->id ] )
8822 ) {
8823 return false;
8824 }
8825
8826 return $addons[ $this->_plugin->id ];
8827 }
8828
8829 /**
8830 * Check if user has any
8831 *
8832 * @author Vova Feldman (@svovaf)
8833 * @since 1.1.6
8834 *
8835 * @return bool
8836 */
8837 function has_account_addons() {
8838 $addons = $this->get_account_addons();
8839
8840 return is_array( $addons ) && ( 0 < count( $addons ) );
8841 }
8842
8843
8844 /**
8845 * Get add-on by ID (from local data).
8846 *
8847 * @author Vova Feldman (@svovaf)
8848 * @since 1.0.6
8849 *
8850 * @param number $id
8851 *
8852 * @return FS_Plugin|false
8853 */
8854 function get_addon( $id ) {
8855 $this->_logger->entrance();
8856
8857 $addons = $this->get_addons();
8858
8859 if ( is_array( $addons ) ) {
8860 foreach ( $addons as $addon ) {
8861 if ( $id == $addon->id ) {
8862 return $addon;
8863 }
8864 }
8865 }
8866
8867 return false;
8868 }
8869
8870 /**
8871 * Get add-on by slug (from local data).
8872 *
8873 * @author Vova Feldman (@svovaf)
8874 * @since 1.0.6
8875 *
8876 * @param string $slug
8877 *
8878 * @param bool $flush
8879 *
8880 * @return FS_Plugin|false
8881 */
8882 function get_addon_by_slug( $slug, $flush = false ) {
8883 $this->_logger->entrance();
8884
8885 $addons = $this->get_addons( $flush );
8886
8887 if ( is_array( $addons ) ) {
8888 foreach ( $addons as $addon ) {
8889 if ( $slug === $addon->slug ) {
8890 return $addon;
8891 }
8892 }
8893 }
8894
8895 return false;
8896 }
8897
8898 /**
8899 * @author Vova Feldman (@svovaf)
8900 * @since 2.0.0
8901 *
8902 * @param number $user_id
8903 *
8904 * @return FS_User
8905 */
8906 static function _get_user_by_id( $user_id ) {
8907 self::$_static_logger->entrance( "user_id = {$user_id}" );
8908
8909 $users = self::get_all_users();
8910
8911 if ( is_array( $users ) ) {
8912 if ( isset( $users[ $user_id ] ) &&
8913 $users[ $user_id ] instanceof FS_User &&
8914 $user_id == $users[ $user_id ]->id
8915 ) {
8916 return $users[ $user_id ];
8917 }
8918
8919 // If user wasn't found by the key, iterate over all the users collection.
8920 foreach ( $users as $user ) {
8921 /**
8922 * @var FS_User $user
8923 */
8924 if ( $user_id == $user->id ) {
8925 return $user;
8926 }
8927 }
8928 }
8929
8930 return null;
8931 }
8932
8933 /**
8934 * Checks if a Freemius user_id is associated with a super-admin.
8935 *
8936 * @author Vova Feldman (@svovaf)
8937 * @since 2.0.0
8938 *
8939 * @param number $user_id
8940 *
8941 * @return bool
8942 */
8943 private static function is_super_admin( $user_id ) {
8944 $is_super_admin = false;
8945
8946 $user = self::_get_user_by_id( $user_id );
8947
8948 if ( $user instanceof FS_User && ! empty( $user->email ) ) {
8949 self::require_pluggable_essentials();
8950
8951 $wp_user = get_user_by( 'email', $user->email );
8952
8953 if ( $wp_user instanceof WP_User ) {
8954 $super_admins = get_super_admins();
8955 $is_super_admin = ( is_array( $super_admins ) && in_array( $wp_user->user_login, $super_admins ) );
8956 }
8957 }
8958
8959 return $is_super_admin;
8960 }
8961
8962 #----------------------------------------------------------------------------------
8963 #region Plans & Licensing
8964 #----------------------------------------------------------------------------------
8965
8966 /**
8967 * Check if running premium plugin code.
8968 *
8969 * @author Vova Feldman (@svovaf)
8970 * @since 1.0.5
8971 *
8972 * @return bool
8973 */
8974 function is_premium() {
8975 return $this->_plugin->is_premium;
8976 }
8977
8978 /**
8979 * Get site's plan ID.
8980 *
8981 * @author Vova Feldman (@svovaf)
8982 * @since 1.0.2
8983 *
8984 * @return number
8985 */
8986 function get_plan_id() {
8987 return $this->_site->plan_id;
8988 }
8989
8990 /**
8991 * Get site's plan title.
8992 *
8993 * @author Vova Feldman (@svovaf)
8994 * @since 1.0.2
8995 *
8996 * @return string
8997 */
8998 function get_plan_title() {
8999 $plan = $this->get_plan();
9000
9001 return is_object( $plan ) ? $plan->title : 'PLAN_TITLE';
9002 }
9003
9004 /**
9005 * Get site's plan name.
9006 *
9007 * @author Vova Feldman (@svovaf)
9008 * @since 2.0.0
9009 *
9010 * @return string
9011 */
9012 function get_plan_name() {
9013 $plan = $this->get_plan();
9014
9015 return is_object( $plan ) ? $plan->name : 'PLAN_NAME';
9016 }
9017
9018 /**
9019 * @author Vova Feldman (@svovaf)
9020 * @since 1.0.9
9021 *
9022 * @return FS_Plugin_Plan|false
9023 */
9024 function get_plan() {
9025 if ( ! is_object( $this->_site ) ) {
9026 return false;
9027 }
9028
9029 return FS_Plugin_Plan::is_valid_id( $this->_site->plan_id ) ?
9030 $this->_get_plan_by_id( $this->_site->plan_id ) :
9031 false;
9032 }
9033
9034 /**
9035 * @author Vova Feldman (@svovaf)
9036 * @since 1.0.3
9037 *
9038 * @return bool
9039 */
9040 function is_trial() {
9041 $this->_logger->entrance();
9042
9043 if ( ! $this->is_registered() ) {
9044 return false;
9045 }
9046
9047 return $this->_site->is_trial();
9048 }
9049
9050 /**
9051 * Check if currently in a trial with payment method (credit card or paypal).
9052 *
9053 * @author Vova Feldman (@svovaf)
9054 * @since 1.1.7
9055 *
9056 * @return bool
9057 */
9058 function is_paid_trial() {
9059 $this->_logger->entrance();
9060
9061 if ( ! $this->is_trial() ) {
9062 return false;
9063 }
9064
9065 return $this->has_active_valid_license() && ( $this->_site->trial_plan_id == $this->_license->plan_id );
9066 }
9067
9068 /**
9069 * Check if trial already utilized.
9070 *
9071 * @since 1.0.9
9072 *
9073 * @return bool
9074 */
9075 function is_trial_utilized() {
9076 $this->_logger->entrance();
9077
9078 if ( ! $this->is_registered() ) {
9079 return false;
9080 }
9081
9082 return $this->_site->is_trial_utilized();
9083 }
9084
9085 /**
9086 * Get trial plan information (if in trial).
9087 *
9088 * @author Vova Feldman (@svovaf)
9089 * @since 1.0.9
9090 *
9091 * @return bool|FS_Plugin_Plan
9092 */
9093 function get_trial_plan() {
9094 $this->_logger->entrance();
9095
9096 if ( ! $this->is_trial() ) {
9097 return false;
9098 }
9099
9100 // Try to load plan from local cache.
9101 $trial_plan = $this->_get_plan_by_id( $this->_site->trial_plan_id );
9102
9103 if ( ! is_object( $trial_plan ) ) {
9104 $trial_plan = $this->_fetch_site_plan( $this->_site->trial_plan_id );
9105
9106 /**
9107 * If managed to fetch the plan, add it to the plans collection.
9108 */
9109 if ( $trial_plan instanceof FS_Plugin_Plan ) {
9110 if ( ! is_array( $this->_plans ) ) {
9111 $this->_plans = array();
9112 }
9113
9114 $this->_plans[] = $trial_plan;
9115 $this->_store_plans();
9116 }
9117 }
9118
9119 if ( $trial_plan instanceof FS_Plugin_Plan ) {
9120 return $trial_plan;
9121 }
9122
9123 /**
9124 * If for some reason failed to get the trial plan, fallback to a dummy name and title.
9125 */
9126 $trial_plan = new FS_Plugin_Plan();
9127 $trial_plan->id = $this->_site->trial_plan_id;
9128 $trial_plan->name = 'pro';
9129 $trial_plan->title = 'Pro';
9130
9131 return $trial_plan;
9132 }
9133
9134 /**
9135 * Check if the user has an activate, non-expired license on current plugin's install.
9136 *
9137 * @since 1.0.9
9138 *
9139 * @return bool
9140 */
9141 function is_paying() {
9142 $this->_logger->entrance();
9143
9144 if ( ! $this->is_registered() ) {
9145 return false;
9146 }
9147
9148 if ( ! $this->has_paid_plan() ) {
9149 return false;
9150 }
9151
9152 return (
9153 ! $this->is_trial() &&
9154 'free' !== $this->get_plan_name() &&
9155 $this->has_active_valid_license()
9156 );
9157 }
9158
9159 /**
9160 * @author Vova Feldman (@svovaf)
9161 * @since 1.0.4
9162 *
9163 * @return bool
9164 */
9165 function is_free_plan() {
9166 if ( ! $this->is_registered() ) {
9167 return true;
9168 }
9169
9170 if ( ! $this->has_paid_plan() ) {
9171 return true;
9172 }
9173
9174 return (
9175 'free' === $this->get_plan_name() ||
9176 ! $this->has_features_enabled_license()
9177 );
9178 }
9179
9180 /**
9181 * @author Vova Feldman (@svovaf)
9182 * @since 1.0.5
9183 *
9184 * @return bool
9185 */
9186 function _has_premium_license() {
9187 $this->_logger->entrance();
9188
9189 $premium_license = $this->_get_available_premium_license();
9190
9191 return ( false !== $premium_license );
9192 }
9193
9194 /**
9195 * Check if user has any licenses associated with the plugin (including expired or blocking).
9196 *
9197 * @author Vova Feldman (@svovaf)
9198 * @since 1.1.7.3
9199 *
9200 * @return bool
9201 */
9202 private function has_any_license() {
9203 return is_array( $this->_licenses ) && ( 0 < count( $this->_licenses ) );
9204 }
9205
9206 /**
9207 * @author Vova Feldman (@svovaf)
9208 * @since 1.0.5
9209 *
9210 * @param bool|null $is_localhost
9211 *
9212 * @return FS_Plugin_License|false
9213 */
9214 function _get_available_premium_license( $is_localhost = null ) {
9215 $this->_logger->entrance();
9216
9217 $licenses = $this->get_available_premium_licenses( $is_localhost );
9218 if ( ! empty( $licenses ) ) {
9219 return $licenses[0];
9220 }
9221
9222 return false;
9223 }
9224
9225 /**
9226 * @author Vova Feldman (@svovaf)
9227 * @since 1.0.5
9228 *
9229 * @param bool|null $is_localhost
9230 *
9231 * @return FS_Plugin_License[]
9232 */
9233 function get_available_premium_licenses( $is_localhost = null ) {
9234 $this->_logger->entrance();
9235
9236 $licenses = array();
9237 if ( ! $this->has_paid_plan() ) {
9238 return $licenses;
9239 }
9240
9241 if ( is_array( $this->_licenses ) ) {
9242 foreach ( $this->_licenses as $license ) {
9243 if ( ! $license->can_activate( $is_localhost ) ) {
9244 continue;
9245 }
9246
9247 $licenses[] = $license;
9248 }
9249 }
9250
9251 return $licenses;
9252 }
9253
9254 /**
9255 * Sync local plugin plans with remote server.
9256 *
9257 * 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).
9258 *
9259 * @author Vova Feldman (@svovaf)
9260 * @since 1.0.5
9261 *
9262 * @return FS_Plugin_Plan[]|object
9263 */
9264 function _sync_plans() {
9265 $plans = $this->_fetch_plugin_plans();
9266
9267 if ( $this->is_array_instanceof( $plans, 'FS_Plugin_Plan' ) ) {
9268 $plans_map = array();
9269 foreach ( $plans as $plan ) {
9270 $plans_map[ $plan->id ] = true;
9271 }
9272
9273 $plans_ids_to_keep = $this->get_plans_ids_associated_with_installs();
9274
9275 foreach ( $plans_ids_to_keep as $plan_id ) {
9276 if ( isset( $plans_map[ $plan_id ] ) ) {
9277 continue;
9278 }
9279
9280 $missing_plan = self::_get_plan_by_id( $plan_id );
9281
9282 if ( is_object( $missing_plan ) ) {
9283 $plans[] = $missing_plan;
9284 }
9285 }
9286
9287 $this->_plans = $plans;
9288 $this->_store_plans();
9289 }
9290
9291 $this->do_action( 'after_plans_sync', $plans );
9292
9293 return $this->_plans;
9294 }
9295
9296 /**
9297 * Check if specified plan exists locally. If not, fetch it and store it.
9298 *
9299 * @author Vova Feldman (@svovaf)
9300 * @since 2.0.0
9301 *
9302 * @param number $plan_id
9303 *
9304 * @return \FS_Plugin_Plan|object The plan entity or the API error object on failure.
9305 */
9306 private function sync_plan_if_not_exist( $plan_id ) {
9307 $plan = self::_get_plan_by_id( $plan_id );
9308
9309 if ( is_object( $plan ) ) {
9310 // Plan already exists.
9311 return $plan;
9312 }
9313
9314 $plan = $this->fetch_plan_by_id( $plan_id );
9315
9316 if ( $plan instanceof FS_Plugin_Plan ) {
9317 $this->_plans[] = $plan;
9318 $this->_store_plans();
9319
9320 return $plan;
9321 }
9322
9323 return $plan;
9324 }
9325
9326 /**
9327 * Check if specified license exists locally. If not, fetch it and store it.
9328 *
9329 * @author Vova Feldman (@svovaf)
9330 * @since 2.0.0
9331 *
9332 * @param number $license_id
9333 * @param string $license_key
9334 *
9335 * @return \FS_Plugin_Plan|object The plan entity or the API error object on failure.
9336 */
9337 private function sync_license_if_not_exist( $license_id, $license_key ) {
9338 $license = $this->_get_license_by_id( $license_id );
9339
9340 if ( is_object( $license ) ) {
9341 // License already exists.
9342 return $license;
9343 }
9344
9345 $license = $this->fetch_license_by_key( $license_id, $license_key );
9346
9347 if ( $license instanceof FS_Plugin_License ) {
9348 $this->_licenses[] = $license;
9349 $this->_license = $license;
9350 $this->_store_licenses();
9351
9352 return $license;
9353 }
9354
9355 return $license;
9356 }
9357
9358 /**
9359 * Get a collection of unique plan IDs that are associated with any installs in the network.
9360 *
9361 * @author Leo Fajardo (@leorw)
9362 * @since 2.0.0
9363 *
9364 * @return number[]
9365 */
9366 private function get_plans_ids_associated_with_installs() {
9367 if ( ! $this->_is_network_active ) {
9368 if ( ! is_object( $this->_site ) ||
9369 ! FS_Plugin_Plan::is_valid_id( $this->_site->plan_id )
9370 ) {
9371 return array();
9372 }
9373
9374 return array( $this->_site->plan_id );
9375 }
9376
9377 $plan_ids = array();
9378 $sites = self::get_sites();
9379 foreach ( $sites as $site ) {
9380 $blog_id = self::get_site_blog_id( $site );
9381 $install = $this->get_install_by_blog_id( $blog_id );
9382
9383 if ( ! is_object( $install ) ||
9384 ! FS_Plugin_Plan::is_valid_id( $install->plan_id )
9385 ) {
9386 continue;
9387 }
9388
9389 $plan_ids[ $install->plan_id ] = true;
9390 }
9391
9392 return array_keys( $plan_ids );
9393 }
9394
9395 /**
9396 * Get a collection of unique license IDs that are associated with any installs in the network.
9397 *
9398 * @author Leo Fajardo (@leorw)
9399 * @since 2.0.0
9400 *
9401 * @return number[]
9402 */
9403 private function get_license_ids_associated_with_installs() {
9404 if ( ! $this->_is_network_active ) {
9405 if ( ! is_object( $this->_site ) ||
9406 ! FS_Plugin_License::is_valid_id( $this->_site->license_id )
9407 ) {
9408 return array();
9409 }
9410
9411 return array( $this->_site->license_id );
9412 }
9413
9414 $license_ids = array();
9415 $sites = self::get_sites();
9416 foreach ( $sites as $site ) {
9417 $blog_id = self::get_site_blog_id( $site );
9418 $install = $this->get_install_by_blog_id( $blog_id );
9419
9420 if ( ! is_object( $install ) ||
9421 ! FS_Plugin_License::is_valid_id( $install->license_id )
9422 ) {
9423 continue;
9424 }
9425
9426 $license_ids[ $install->license_id ] = true;
9427 }
9428
9429 return array_keys( $license_ids );
9430 }
9431
9432 /**
9433 * @author Vova Feldman (@svovaf)
9434 * @since 1.0.5
9435 *
9436 * @param number $id
9437 *
9438 * @return FS_Plugin_Plan|false
9439 */
9440 function _get_plan_by_id( $id ) {
9441 $this->_logger->entrance();
9442
9443 if ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) {
9444 $this->_sync_plans();
9445 }
9446
9447 foreach ( $this->_plans as $plan ) {
9448 if ( $id == $plan->id ) {
9449 return $plan;
9450 }
9451 }
9452
9453 return false;
9454 }
9455
9456 /**
9457 * @author Vova Feldman (@svovaf)
9458 * @since 1.1.8.1
9459 *
9460 * @param string $name
9461 *
9462 * @return FS_Plugin_Plan|false
9463 */
9464 private function get_plan_by_name( $name ) {
9465 $this->_logger->entrance();
9466
9467 if ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) {
9468 $this->_sync_plans();
9469 }
9470
9471 foreach ( $this->_plans as $plan ) {
9472 if ( $name == $plan->name ) {
9473 return $plan;
9474 }
9475 }
9476
9477 return false;
9478 }
9479
9480 /**
9481 * Sync local licenses with remote server.
9482 *
9483 * @author Vova Feldman (@svovaf)
9484 * @since 1.0.6
9485 *
9486 * @param number|bool $site_license_id
9487 * @param number|null $blog_id
9488 *
9489 * @return FS_Plugin_License[]|object
9490 */
9491 function _sync_licenses( $site_license_id = false, $blog_id = null ) {
9492 $this->_logger->entrance();
9493
9494 $is_network_admin = fs_is_network_admin();
9495
9496 if ( $is_network_admin && is_null( $blog_id ) ) {
9497 $all_licenses = self::get_all_licenses( $this->_module_id );
9498 } else {
9499 $all_licenses = $this->get_user_licenses( $this->_user->id );
9500 }
9501
9502 $foreign_licenses = array(
9503 'ids' => array(),
9504 'license_keys' => array()
9505 );
9506
9507 $all_licenses_map = array();
9508 foreach ( $all_licenses as $license ) {
9509 $all_licenses_map[ $license->id ] = true;
9510 if ( $license->user_id == $this->_user->id || $license->id == $site_license_id ) {
9511 continue;
9512 }
9513
9514 $foreign_licenses['ids'][] = $license->id;
9515 $foreign_licenses['license_keys'][] = $license->secret_key;
9516 }
9517
9518 if ( empty( $foreign_licenses['ids'] ) ) {
9519 $foreign_licenses = array();
9520 }
9521
9522 $licenses = $this->_fetch_licenses( false, $site_license_id, $foreign_licenses, $blog_id );
9523
9524 if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) {
9525 $licenses_map = array();
9526 foreach ( $licenses as $license ) {
9527 $licenses_map[ $license->id ] = true;
9528 }
9529
9530 // $license_ids_to_keep = $this->get_license_ids_associated_with_installs();
9531 // foreach ( $license_ids_to_keep as $license_id ) {
9532 // if ( isset( $licenses_map[ $license_id ] ) ) {
9533 // continue;
9534 // }
9535 //
9536 // $missing_license = self::_get_license_by_id( $license_id, false );
9537 // if ( is_object( $missing_license ) ) {
9538 // $licenses[] = $missing_license;
9539 // $licenses_map[ $missing_license->id ] = true;
9540 // }
9541 // }
9542
9543 $user_license_ids = $this->get_user_linked_license_ids( $this->_user->id );
9544
9545 foreach ( $user_license_ids as $key => $license_id ) {
9546 if ( ! isset( $licenses_map[ $license_id ] ) ) {
9547 // Remove access to licenses that no longer exist.
9548 unset( $user_license_ids[ $key ] );
9549 }
9550 }
9551
9552 if ( ! empty( $user_license_ids ) ) {
9553 foreach ( $licenses_map as $license_id => $value ) {
9554 if ( ! isset( $all_licenses_map[ $license_id ] ) ) {
9555 // Associate new licenses with the user who triggered the license syncing.
9556 $user_license_ids[] = $license_id;
9557 }
9558 }
9559
9560 $user_license_ids = array_unique( $user_license_ids );
9561 } else {
9562 $user_license_ids = array_keys( $licenses_map );
9563 }
9564
9565 if ( ! $is_network_admin || ! is_null( $blog_id ) ) {
9566 $user_licenses = array();
9567 foreach ( $licenses as $license ) {
9568 if ( ! in_array( $license->id, $user_license_ids ) ) {
9569 continue;
9570 }
9571
9572 $user_licenses[] = $license;
9573 }
9574
9575 $this->_licenses = $user_licenses;
9576 } else {
9577 $this->_licenses = $licenses;
9578 }
9579
9580 $this->set_user_linked_license_ids( $this->_user->id, $user_license_ids );
9581
9582 $this->_store_licenses( true, $this->_module_id, $licenses );
9583 }
9584
9585 // Update current license.
9586 if ( is_object( $this->_license ) ) {
9587 $this->_license = $this->_get_license_by_id( $this->_license->id );
9588 }
9589
9590 return $this->_licenses;
9591 }
9592
9593 /**
9594 * @author Vova Feldman (@svovaf)
9595 * @since 1.0.5
9596 *
9597 * @param number $id
9598 * @param bool $sync_licenses
9599 *
9600 * @return FS_Plugin_License|false
9601 */
9602 function _get_license_by_id( $id, $sync_licenses = true ) {
9603 $this->_logger->entrance();
9604
9605 if ( ! FS_Plugin_License::is_valid_id( $id ) ) {
9606 return false;
9607 }
9608
9609 /**
9610 * When running from the network level admin and opted-in from the network,
9611 * check if the license exists in the network user licenses collection.
9612 *
9613 * @author Vova Feldman (@svovaf)
9614 * @since 2.0.0
9615 */
9616 if ( fs_is_network_admin() &&
9617 $this->is_network_registered() &&
9618 ( ! is_object( $this->_user ) || $this->_storage->network_user_id != $this->_user->id )
9619 ) {
9620 $licenses = $this->get_user_licenses( $this->_storage->network_user_id );
9621
9622 foreach ( $licenses as $license ) {
9623 if ( $id == $license->id ) {
9624 return $license;
9625 }
9626 }
9627 }
9628
9629 if ( ! $this->has_any_license() && $sync_licenses ) {
9630 $this->_sync_licenses( $id );
9631 }
9632
9633 if ( is_array( $this->_licenses ) ) {
9634 foreach ( $this->_licenses as $license ) {
9635 if ( $id == $license->id ) {
9636 return $license;
9637 }
9638 }
9639 }
9640
9641 return false;
9642 }
9643
9644 /**
9645 * 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.
9646 *
9647 * @author Vova Feldman (@svovaf)
9648 * @since 2.0.0
9649 *
9650 * @param number $id
9651 *
9652 * @return FS_Plugin_License
9653 */
9654 private function get_license_by_id( $id ) {
9655 $licenses = self::get_all_licenses( $this->_module_id );
9656
9657 if ( is_array( $licenses ) && ! empty( $licenses ) ) {
9658 foreach ( $licenses as $license ) {
9659 if ( $id == $license->id ) {
9660 return $license;
9661 }
9662 }
9663 }
9664
9665 return null;
9666 }
9667
9668 /**
9669 * Synchronize the site's context license by fetching the license form the API and updating the local data with it.
9670 *
9671 * @author Vova Feldman (@svovaf)
9672 * @since 2.0.0
9673 *
9674 * @return \FS_Plugin_License|mixed
9675 */
9676 private function sync_site_license() {
9677 $api = $this->get_api_user_scope();
9678
9679 $result = $api->get( "/licenses/{$this->_license->id}.json?license_key=" . urlencode( $this->_license->secret_key ), true );
9680
9681 if ( ! $this->is_api_result_entity( $result ) ) {
9682 return $result;
9683 }
9684
9685 $license = $this->_update_site_license( new FS_Plugin_License( $result ) );
9686 $this->_store_licenses();
9687
9688 return $license;
9689 }
9690
9691 /**
9692 * Get all user's available licenses for the current module.
9693 *
9694 * @author Vova Feldman (@svovaf)
9695 * @since 2.0.0
9696 *
9697 * @param number $user_id
9698 *
9699 * @return FS_Plugin_License[]
9700 */
9701 private function get_user_licenses( $user_id ) {
9702 $all_licenses = self::get_all_licenses( $this->_module_id );
9703 if ( empty( $all_licenses ) ) {
9704 return array();
9705 }
9706
9707 $user_license_ids = $this->get_user_linked_license_ids( $user_id );
9708 if ( empty( $user_license_ids ) ) {
9709 return array();
9710 }
9711
9712 $licenses = array();
9713 foreach ( $all_licenses as $license ) {
9714 if ( in_array( $license->id, $user_license_ids ) ) {
9715 $licenses[] = $license;
9716 }
9717 }
9718
9719 return $licenses;
9720 }
9721
9722 /**
9723 * Checks if the context license is network activated except on the given blog ID.
9724 *
9725 * @author Vova Feldman (@svovaf)
9726 * @since 2.0.0
9727 *
9728 * @param int $except_blog_id
9729 *
9730 * @return bool
9731 */
9732 private function is_license_network_active( $except_blog_id = 0 ) {
9733 $this->_logger->entrance();
9734
9735 if ( ! is_object( $this->_license ) ) {
9736 return false;
9737 }
9738
9739 $sites = self::get_sites();
9740
9741 if ( $this->_license->total_activations() < ( count( $sites ) - 1 ) ) {
9742 // There are more sites than the number of activations, so license cannot be network activated.
9743 return false;
9744 }
9745
9746 foreach ( $sites as $site ) {
9747 $blog_id = self::get_site_blog_id( $site );
9748
9749 if ( $except_blog_id == $blog_id ) {
9750 // Skip excluded blog.
9751 continue;
9752 }
9753
9754 $install = $this->get_install_by_blog_id( $blog_id );
9755
9756 if ( is_object( $install ) && $install->license_id != $this->_license->id ) {
9757 return false;
9758 }
9759 }
9760
9761 return true;
9762 }
9763
9764 /**
9765 * 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.
9766 *
9767 * Notice: On success, this method will also update the license activations counters (without updating the license in the storage).
9768 *
9769 * @author Vova Feldman (@svovaf)
9770 * @since 2.0.0
9771 *
9772 * @param \FS_User $user
9773 * @param \FS_Plugin_License $license
9774 *
9775 * @return bool
9776 */
9777 private function try_activate_license_on_network( FS_User $user, FS_Plugin_License $license ) {
9778 $this->_logger->entrance();
9779
9780 $result = $this->can_activate_license_on_network( $license );
9781
9782 if ( false === $result ) {
9783 return false;
9784 }
9785
9786 $installs_without_license = $result['installs'];
9787 if ( ! empty( $installs_without_license ) ) {
9788 $this->activate_license_on_many_installs( $user, $license->secret_key, $installs_without_license );
9789 }
9790
9791 $disconnected_site_ids = $result['sites'];
9792 if ( ! empty( $disconnected_site_ids ) ) {
9793 $this->activate_license_on_many_sites( $user, $license->secret_key, $disconnected_site_ids );
9794 }
9795
9796 $this->link_license_2_user( $license->id, $user->id );
9797
9798 // Sync license after activations.
9799 $license->activated += $result['production_count'];
9800 $license->activated_local += $result['localhost_count'];
9801
9802 // $this->_store_licenses()
9803
9804 return true;
9805 }
9806
9807 /**
9808 * Checks if the given license can be activated on the whole network.
9809 *
9810 * @author Vova Feldman (@svovaf)
9811 * @since 2.0.0
9812 *
9813 * @param \FS_Plugin_License $license
9814 *
9815 * @return false|array {
9816 * @type array[int]FS_Site $installs Blog ID to install map.
9817 * @type int[] $sites Non-connected blog IDs.
9818 * @type int $production_count Production sites count.
9819 * @type int $localhost_count Production sites count.
9820 * }
9821 */
9822 private function can_activate_license_on_network( FS_Plugin_License $license ) {
9823 $sites = self::get_sites();
9824
9825 $production_count = 0;
9826 $localhost_count = 0;
9827
9828 $installs_without_license = array();
9829 $disconnected_site_ids = array();
9830
9831 foreach ( $sites as $site ) {
9832 $blog_id = self::get_site_blog_id( $site );
9833 $install = $this->get_install_by_blog_id( $blog_id );
9834
9835 if ( is_object( $install ) ) {
9836 if ( FS_Plugin_License::is_valid_id( $install->license_id ) ) {
9837 // License already activated on the install.
9838 continue;
9839 }
9840
9841 $url = $install->url;
9842
9843 $installs_without_license[ $blog_id ] = $install;
9844 } else {
9845 $url = is_object( $site ) ?
9846 $site->siteurl :
9847 get_site_url( $blog_id );
9848
9849 $disconnected_site_ids[] = $blog_id;
9850 }
9851
9852 if ( FS_Site::is_localhost_by_address( $url ) ) {
9853 $localhost_count ++;
9854 } else {
9855 $production_count ++;
9856 }
9857 }
9858
9859 if ( ! $license->can_activate_bulk( $production_count, $localhost_count ) ) {
9860 return false;
9861 }
9862
9863 return array(
9864 'installs' => $installs_without_license,
9865 'sites' => $disconnected_site_ids,
9866 'production_count' => $production_count,
9867 'localhost_count' => $localhost_count,
9868 );
9869 }
9870
9871 /**
9872 * Activate a given license on a collection of installs.
9873 *
9874 * @author Vova Feldman (@svovaf)
9875 * @since 2.0.0
9876 *
9877 * @param \FS_User $user
9878 * @param string $license_key
9879 * @param array $blog_2_install_map {
9880 * @key int Blog ID.
9881 * @value FS_Site Blog's associated install.
9882 * }
9883 *
9884 * @return mixed|true
9885 */
9886 private function activate_license_on_many_installs(
9887 FS_User $user,
9888 $license_key,
9889 array $blog_2_install_map
9890 ) {
9891 $params = array(
9892 array( 'license_key' => $this->apply_filters( 'license_key', $license_key ) )
9893 );
9894
9895 $install_2_blog_map = array();
9896 foreach ( $blog_2_install_map as $blog_id => $install ) {
9897 $params[] = array( 'id' => $install->id );
9898
9899 $install_2_blog_map[ $install->id ] = $blog_id;
9900 }
9901
9902 $result = $this->get_api_user_scope_by_user( $user )->call(
9903 "plugins/{$this->_plugin->id}/installs.json",
9904 'PUT',
9905 $params
9906 );
9907
9908 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
9909 return $result;
9910 }
9911
9912 foreach ( $result->installs as $r_install ) {
9913 $install = new FS_Site( $r_install );
9914 $install->is_disconnected = false;
9915
9916 // Update install.
9917 $this->_store_site(
9918 true,
9919 $install_2_blog_map[ $r_install->id ],
9920 $install
9921 );
9922 }
9923
9924 return true;
9925 }
9926
9927 /**
9928 * Activate a given license on a collection of blogs/sites that are not yet opted-in.
9929 *
9930 * @author Vova Feldman (@svovaf)
9931 * @since 2.0.0
9932 *
9933 * @param \FS_User $user
9934 * @param string $license_key
9935 * @param int[] $site_ids
9936 *
9937 * @return true|mixed True if successful, otherwise, the API result.
9938 */
9939 private function activate_license_on_many_sites(
9940 FS_User $user,
9941 $license_key,
9942 array $site_ids
9943 ) {
9944 $sites = array();
9945 foreach ( $site_ids as $site_id ) {
9946 $sites[] = $this->get_site_info( array( 'blog_id' => $site_id ) );
9947 }
9948
9949 // Install the plugin.
9950 $result = $this->create_installs_with_user(
9951 $user,
9952 $license_key,
9953 false,
9954 $sites,
9955 false,
9956 true
9957 );
9958
9959 if ( ! $this->is_api_result_entity( $result ) &&
9960 ! $this->is_api_result_object( $result, 'installs' )
9961 ) {
9962 return $result;
9963 }
9964
9965 $installs = array();
9966 foreach ( $result->installs as $install ) {
9967 $installs[] = new FS_Site( $install );
9968 }
9969
9970 // Map site addresses to their blog IDs.
9971 $address_to_blog_map = $this->get_address_to_blog_map();
9972
9973 $first_blog_id = null;
9974
9975 foreach ( $installs as $install ) {
9976 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
9977 $blog_id = $address_to_blog_map[ $address ];
9978
9979 $this->_store_site( true, $blog_id, $install );
9980
9981 $this->reset_anonymous_mode( $blog_id );
9982
9983 if ( is_null( $first_blog_id ) ) {
9984 $first_blog_id = $blog_id;
9985 }
9986 }
9987
9988 if ( ! FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ) {
9989 $this->_storage->network_install_blog_id = $first_blog_id;
9990 }
9991
9992 return true;
9993 }
9994
9995 /**
9996 * Sync site's license with user licenses.
9997 *
9998 * @author Vova Feldman (@svovaf)
9999 * @since 1.0.6
10000 *
10001 * @param FS_Plugin_License|null $new_license
10002 *
10003 * @return FS_Plugin_License|null
10004 */
10005 function _update_site_license( $new_license ) {
10006 $this->_logger->entrance();
10007
10008 $this->_license = $new_license;
10009
10010 if ( ! is_object( $new_license ) ) {
10011 $this->_site->license_id = null;
10012 $this->_sync_site_subscription( null );
10013
10014 return $this->_license;
10015 }
10016
10017 $this->_site->license_id = $this->_license->id;
10018
10019 if ( ! is_array( $this->_licenses ) ) {
10020 $this->_licenses = array();
10021 }
10022
10023 $is_license_found = false;
10024 for ( $i = 0, $len = count( $this->_licenses ); $i < $len; $i ++ ) {
10025 if ( $new_license->id == $this->_licenses[ $i ]->id ) {
10026 $this->_licenses[ $i ] = $new_license;
10027
10028 $is_license_found = true;
10029 break;
10030 }
10031 }
10032
10033 // If new license just append.
10034 if ( ! $is_license_found ) {
10035 $this->_licenses[] = $new_license;
10036 }
10037
10038 $this->_sync_site_subscription( $new_license );
10039
10040 return $this->_license;
10041 }
10042
10043 /**
10044 * Sync site's subscription.
10045 *
10046 * @author Vova Feldman (@svovaf)
10047 * @since 1.0.9
10048 *
10049 * @param FS_Plugin_License|null $license
10050 *
10051 * @return bool|\FS_Subscription
10052 */
10053 private function _sync_site_subscription( $license ) {
10054 if ( ! is_object( $license ) ) {
10055 $this->delete_unused_subscriptions();
10056
10057 return false;
10058 }
10059
10060 // Load subscription details if not lifetime.
10061 $subscription = $license->is_lifetime() ?
10062 false :
10063 $this->_fetch_site_license_subscription();
10064
10065 if ( is_object( $subscription ) && ! isset( $subscription->error ) ) {
10066 $this->store_subscription( $subscription );
10067 } else {
10068 $this->delete_unused_subscriptions();
10069 }
10070
10071 return $subscription;
10072 }
10073
10074 /**
10075 * @author Vova Feldman (@svovaf)
10076 * @since 1.0.6
10077 *
10078 * @return bool|\FS_Plugin_License
10079 */
10080 function _get_license() {
10081 if ( ! fs_is_network_admin() || is_object( $this->_license ) ) {
10082 return $this->_license;
10083 }
10084
10085 return $this->_get_available_premium_license();
10086 }
10087
10088 /**
10089 * @param number $license_id
10090 *
10091 * @return null|\FS_Subscription
10092 */
10093 function _get_subscription( $license_id ) {
10094 if ( ! isset( $this->_storage->subscriptions ) ||
10095 empty( $this->_storage->subscriptions )
10096 ) {
10097 return null;
10098 }
10099
10100 foreach ( $this->_storage->subscriptions as $subscription ) {
10101 if ( $subscription->license_id == $license_id ) {
10102 return $subscription;
10103 }
10104 }
10105
10106 return null;
10107 }
10108
10109 /**
10110 * @author Leo Fajardo (@leorw)
10111 * @since 2.0.0
10112 *
10113 * @param FS_Subscription $subscription
10114 */
10115 function store_subscription( FS_Subscription $subscription ) {
10116 if ( ! isset( $this->_storage->subscriptions ) ) {
10117 $this->_storage->subscriptions = array();
10118 }
10119
10120 if ( empty( $this->_storage->subscriptions ) || ! is_multisite() ) {
10121 $this->_storage->subscriptions = array( $subscription );
10122
10123 return;
10124 }
10125
10126 $subscriptions = $this->_storage->subscriptions;
10127
10128 $updated_subscription = false;
10129 foreach ( $subscriptions as $key => $existing_subscription ) {
10130 if ( $existing_subscription->id == $subscription->id ) {
10131 $subscriptions[ $key ] = $subscription;
10132 $updated_subscription = true;
10133 break;
10134 }
10135 }
10136
10137 if ( ! $updated_subscription ) {
10138 $subscriptions[] = $subscription;
10139 }
10140
10141 $this->_storage->subscriptions = $subscriptions;
10142 }
10143
10144 /**
10145 * @author Leo Fajardo (@leorw)
10146 * @since 2.0.0
10147 */
10148 function delete_unused_subscriptions() {
10149 if ( ! isset( $this->_storage->subscriptions ) ||
10150 empty( $this->_storage->subscriptions ) ||
10151 // Clean up only if there are already at least 3 subscriptions.
10152 ( count( $this->_storage->subscriptions ) < 3 )
10153 ) {
10154 return;
10155 }
10156
10157 if ( ! is_multisite() ) {
10158 // If not multisite, there should only be 1 subscription, so just clear the array.
10159 $this->_storage->subscriptions = array();
10160
10161 return;
10162 }
10163
10164 $subscriptions_to_keep_by_license_id_map = array();
10165 $sites = self::get_sites();
10166 foreach ( $sites as $site ) {
10167 $blog_id = self::get_site_blog_id( $site );
10168 $install = $this->get_install_by_blog_id( $blog_id );
10169
10170 if ( ! is_object( $install ) ||
10171 ! FS_Plugin_License::is_valid_id( $install->license_id )
10172 ) {
10173 continue;
10174 }
10175
10176 $subscriptions_to_keep_by_license_id_map[ $install->license_id ] = true;
10177 }
10178
10179 if ( empty( $subscriptions_to_keep_by_license_id_map ) ) {
10180 $this->_storage->subscriptions = array();
10181
10182 return;
10183 }
10184
10185 foreach ( $this->_storage->subscriptions as $key => $subscription ) {
10186 if ( ! isset( $subscriptions_to_keep_by_license_id_map[ $subscription->license_id ] ) ) {
10187 unset( $this->_storage->subscriptions[ $key ] );
10188 }
10189 }
10190 }
10191
10192 /**
10193 * @author Vova Feldman (@svovaf)
10194 * @since 1.0.2
10195 *
10196 * @param string $plan Plan name
10197 * @param bool $exact If true, looks for exact plan. If false, also check "higher" plans.
10198 *
10199 * @return bool
10200 */
10201 function is_plan( $plan, $exact = false ) {
10202 $this->_logger->entrance();
10203
10204 if ( ! $this->is_registered() ) {
10205 return false;
10206 }
10207
10208 $plan = strtolower( $plan );
10209
10210 $current_plan_name = $this->get_plan_name();
10211
10212 if ( $current_plan_name === $plan ) {
10213 // Exact plan.
10214 return true;
10215 } else if ( $exact ) {
10216 // Required exact, but plans are different.
10217 return false;
10218 }
10219
10220 $current_plan_order = - 1;
10221 $required_plan_order = - 1;
10222 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
10223 if ( $plan === $this->_plans[ $i ]->name ) {
10224 $required_plan_order = $i;
10225 } else if ( $current_plan_name === $this->_plans[ $i ]->name ) {
10226 $current_plan_order = $i;
10227 }
10228 }
10229
10230 return ( $current_plan_order > $required_plan_order );
10231 }
10232
10233 /**
10234 * Check if module has only one plan.
10235 *
10236 * @author Vova Feldman (@svovaf)
10237 * @since 1.2.1.7
10238 *
10239 * @return bool
10240 */
10241 function is_single_plan() {
10242 $this->_logger->entrance();
10243
10244 if ( ! $this->is_registered() ||
10245 ! is_array( $this->_plans ) ||
10246 0 === count( $this->_plans )
10247 ) {
10248 return true;
10249 }
10250
10251 return ( 1 === count( $this->_plans ) );
10252 }
10253
10254 /**
10255 * Check if plan based on trial. If not in trial mode, should return false.
10256 *
10257 * @since 1.0.9
10258 *
10259 * @param string $plan Plan name
10260 * @param bool $exact If true, looks for exact plan. If false, also check "higher" plans.
10261 *
10262 * @return bool
10263 */
10264 function is_trial_plan( $plan, $exact = false ) {
10265 $this->_logger->entrance();
10266
10267 if ( ! $this->is_registered() ) {
10268 return false;
10269 }
10270
10271 if ( ! $this->is_trial() ) {
10272 return false;
10273 }
10274
10275 $trial_plan = $this->get_trial_plan();
10276
10277 if ( $trial_plan->name === $plan ) {
10278 // Exact plan.
10279 return true;
10280 } else if ( $exact ) {
10281 // Required exact, but plans are different.
10282 return false;
10283 }
10284
10285 $current_plan_order = - 1;
10286 $required_plan_order = - 1;
10287 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
10288 if ( $plan === $this->_plans[ $i ]->name ) {
10289 $required_plan_order = $i;
10290 } else if ( $trial_plan->name === $this->_plans[ $i ]->name ) {
10291 $current_plan_order = $i;
10292 }
10293 }
10294
10295 return ( $current_plan_order > $required_plan_order );
10296 }
10297
10298 /**
10299 * Check if plugin has any paid plans.
10300 *
10301 * @author Vova Feldman (@svovaf)
10302 * @since 1.0.7
10303 *
10304 * @return bool
10305 */
10306 function has_paid_plan() {
10307 return $this->_has_paid_plans ||
10308 FS_Plan_Manager::instance()->has_paid_plan( $this->_plans );
10309 }
10310
10311 /**
10312 * Check if plugin has any plan with a trail.
10313 *
10314 * @author Vova Feldman (@svovaf)
10315 * @since 1.0.9
10316 *
10317 * @return bool
10318 */
10319 function has_trial_plan() {
10320 /**
10321 * @author Vova Feldman(@svovaf)
10322 * @since 1.2.1.5
10323 *
10324 * Allow setting a trial from the SDK without calling the API.
10325 * But, if the user did opt-in, continue using the real data from the API.
10326 */
10327 if ( $this->_trial_days >= 0 ) {
10328 return true;
10329 }
10330
10331 return $this->_storage->get( 'has_trial_plan', false );
10332 }
10333
10334 /**
10335 * Check if plugin has any free plan, or is it premium only.
10336 *
10337 * Note: If no plans configured, assume plugin is free.
10338 *
10339 * @author Vova Feldman (@svovaf)
10340 * @since 1.0.7
10341 *
10342 * @return bool
10343 */
10344 function has_free_plan() {
10345 return ! $this->is_only_premium();
10346 }
10347
10348 /**
10349 * Displays a license activation dialog box when the user clicks on the "Activate License"
10350 * or "Change License" link on the plugins
10351 * page.
10352 *
10353 * @author Leo Fajardo (@leorw)
10354 * @since 1.1.9
10355 */
10356 function _add_license_activation_dialog_box() {
10357 $vars = array(
10358 'id' => $this->_module_id,
10359 );
10360
10361 fs_require_template( 'forms/license-activation.php', $vars );
10362 fs_require_template( 'forms/resend-key.php', $vars );
10363 }
10364
10365 /**
10366 * Displays the opt-out dialog box when the user clicks on the "Opt Out" link on the "Plugins"
10367 * page.
10368 *
10369 * @author Leo Fajardo (@leorw)
10370 * @since 1.2.1.5
10371 */
10372 function _add_optout_dialog() {
10373 if ( $this->is_theme() ) {
10374 $vars = null;
10375 fs_require_once_template( '/js/jquery.content-change.php', $vars );
10376 }
10377
10378 $vars = array( 'id' => $this->_module_id );
10379 fs_require_template( 'forms/optout.php', $vars );
10380 }
10381
10382 /**
10383 * Prepare page to include all required UI and logic for the license activation dialog.
10384 *
10385 * @author Vova Feldman (@svovaf)
10386 * @since 1.2.0
10387 */
10388 function _add_license_activation() {
10389 if ( ! $this->is_user_admin() ) {
10390 // Only admins can activate a license.
10391 return;
10392 }
10393
10394 if ( ! $this->has_paid_plan() ) {
10395 // Module doesn't have any paid plans.
10396 return;
10397 }
10398
10399 if ( ! $this->is_premium() ) {
10400 // Only add license activation logic to the premium version.
10401 return;
10402 }
10403
10404 // Add license activation link and AJAX request handler.
10405 if ( self::is_plugins_page() ) {
10406 /**
10407 * @since 1.2.0 Add license action link only on plugins page.
10408 */
10409 $this->_add_license_action_link();
10410 }
10411
10412 // Add license activation AJAX callback.
10413 $this->add_ajax_action( 'activate_license', array( &$this, '_activate_license_ajax_action' ) );
10414
10415 // Add resend license AJAX callback.
10416 $this->add_ajax_action( 'resend_license_key', array( &$this, '_resend_license_key_ajax_action' ) );
10417 }
10418
10419 /**
10420 * @author Leo Fajardo (@leorw)
10421 *
10422 * @since 1.1.9
10423 * @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).
10424 */
10425 function _activate_license_ajax_action() {
10426 $this->_logger->entrance();
10427
10428 $this->check_ajax_referer( 'activate_license' );
10429
10430 $license_key = trim( fs_request_get( 'license_key' ) );
10431
10432 if ( empty( $license_key ) ) {
10433 exit;
10434 }
10435
10436 $plugin_id = fs_request_get( 'module_id', '', 'post' );
10437 $fs = ( $plugin_id == $this->_module_id ) ?
10438 $this :
10439 $this->get_addon_instance( $plugin_id );
10440
10441 $error = false;
10442 $next_page = false;
10443
10444 $sites = fs_is_network_admin() ?
10445 fs_request_get( 'sites', array(), 'post' ) :
10446 array();
10447
10448 $blog_id = fs_request_get( 'blog_id' );
10449 $has_valid_blog_id = is_numeric( $blog_id );
10450
10451 if ( $fs->is_registered() ) {
10452 if ( fs_is_network_admin() && ! $has_valid_blog_id ) {
10453 // If no specific blog ID was provided, activate the license for all sites in the network.
10454 $blog_2_install_map = array();
10455 $site_ids = array();
10456
10457 foreach ( $sites as $site ) {
10458 if ( ! isset( $site['blog_id'] ) || ! is_numeric( $site['blog_id'] ) ) {
10459 continue;
10460 }
10461
10462 $install = $this->get_install_by_blog_id( $site['blog_id'] );
10463
10464 if ( is_object( $install ) ) {
10465 $blog_2_install_map[ $site['blog_id'] ] = $install;
10466 } else {
10467 $site_ids[] = $site['blog_id'];
10468 }
10469 }
10470
10471 $user = $this->get_current_or_network_user();
10472
10473 if ( ! empty( $blog_2_install_map ) ) {
10474 $result = $this->activate_license_on_many_installs( $user, $license_key, $blog_2_install_map );
10475
10476 if ( true !== $result ) {
10477 $error = FS_Api::is_api_error_object( $result ) ?
10478 $result->error->message :
10479 var_export( $result, true );
10480 }
10481 }
10482
10483 if ( empty( $error ) && ! empty( $site_ids ) ) {
10484 $result = $this->activate_license_on_many_sites( $user, $license_key, $site_ids );
10485
10486 if ( true !== $result ) {
10487 $error = FS_Api::is_api_error_object( $result ) ?
10488 $result->error->message :
10489 var_export( $result, true );
10490 }
10491 }
10492 } else {
10493 if ( $has_valid_blog_id ) {
10494 /**
10495 * If a specific blog ID was provided, activate the license only for the install that is
10496 * associated with the given blog ID.
10497 *
10498 * @author Leo Fajardo (@leorw)
10499 */
10500 $this->switch_to_blog( $blog_id );
10501 }
10502
10503 $api = $fs->get_api_site_scope();
10504
10505 $params = array(
10506 'license_key' => $fs->apply_filters( 'license_key', $license_key )
10507 );
10508
10509 $install = $api->call( '/', 'put', $params );
10510
10511 if ( FS_Api::is_api_error( $install ) ) {
10512 $error = FS_Api::is_api_error_object( $install ) ?
10513 $install->error->message :
10514 var_export( $install->error, true );
10515 } else {
10516 $fs->reconnect_locally( $has_valid_blog_id );
10517 }
10518 }
10519
10520 if ( empty( $error ) ) {
10521 $this->network_upgrade_mode_completed();
10522
10523 $fs->_sync_license( true, $has_valid_blog_id );
10524
10525 $next_page = $fs->is_addon() ?
10526 $fs->get_parent_instance()->get_account_url() :
10527 $fs->get_account_url();
10528 }
10529 } else {
10530 $next_page = $fs->opt_in(
10531 false,
10532 false,
10533 false,
10534 $license_key,
10535 false,
10536 false,
10537 false,
10538 $sites
10539 );
10540
10541 if ( isset( $next_page->error ) ) {
10542 $error = $next_page->error;
10543 } else {
10544 if ( fs_is_network_admin() ) {
10545 /**
10546 * Get the list of sites that were just opted-in (and license activated).
10547 * This is an optimization for the next part below saving some DB queries.
10548 */
10549 $connected_sites = array();
10550 foreach ( $sites as $site ) {
10551 if ( isset( $site['blog_id'] ) && is_numeric( $site['blog_id'] ) ) {
10552 $connected_sites[ $site['blog_id'] ] = true;
10553 }
10554 }
10555
10556 $all_sites = self::get_sites();
10557 $pending_sites = array();
10558
10559 /**
10560 * 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.
10561 *
10562 * @author Vova Feldman (@svovaf)
10563 */
10564 foreach ( $all_sites as $site ) {
10565 $blog_id = self::get_site_blog_id( $site );
10566
10567 if ( isset( $connected_sites[ $blog_id ] ) ) {
10568 // Site was just connected.
10569 continue;
10570 }
10571
10572 if ( $this->is_installed_on_site( $blog_id ) ) {
10573 // Site was already connected before.
10574 continue;
10575 }
10576
10577 if ( $this->is_site_delegated_connection( $blog_id ) ) {
10578 // Site's connection was delegated.
10579 continue;
10580 }
10581
10582 if ( $this->is_anonymous_site( $blog_id ) ) {
10583 // Site connection was already skipped.
10584 continue;
10585 }
10586
10587 $pending_sites[] = self::get_site_info( $site );
10588 }
10589
10590 if ( ! empty( $pending_sites ) ) {
10591 if ( $this->is_freemium() ) {
10592 $this->skip_connection( $pending_sites );
10593 } else {
10594 $this->delegate_connection( $pending_sites );
10595 }
10596 }
10597 }
10598 }
10599 }
10600
10601 $result = array(
10602 'success' => ( false === $error )
10603 );
10604
10605 if ( false !== $error ) {
10606 $result['error'] = $error;
10607 } else {
10608 $result['next_page'] = $next_page;
10609 }
10610
10611 echo json_encode( $result );
10612
10613 exit;
10614 }
10615
10616 /**
10617 * @author Leo Fajardo (@leorw)
10618 * @since 1.2.3.1
10619 */
10620 function _network_activate_ajax_action() {
10621 $this->_logger->entrance();
10622
10623 $this->check_ajax_referer( 'network_activate' );
10624
10625 $plugin_id = fs_request_get( 'module_id', '', 'post' );
10626 $fs = ( $plugin_id == $this->_module_id ) ?
10627 $this :
10628 $this->get_addon_instance( $plugin_id );
10629
10630 $error = false;
10631
10632 $sites = fs_request_get( 'sites', array(), 'post' );
10633 if ( is_array( $sites ) && ! empty( $sites ) ) {
10634 $sites_by_action = array(
10635 'allow' => array(),
10636 'delegate' => array(),
10637 'skip' => array()
10638 );
10639
10640 foreach ( $sites as $site ) {
10641 $sites_by_action[ $site['action'] ][] = $site;
10642 }
10643
10644 $total_sites = count( $sites );
10645 $total_sites_to_delegate = count( $sites_by_action['delegate'] );
10646
10647 $next_page = '';
10648 if ( $total_sites === $total_sites_to_delegate &&
10649 ! $this->is_network_upgrade_mode()
10650 ) {
10651 $this->delegate_connection();
10652 } else {
10653 if ( ! empty( $sites_by_action['delegate'] ) ) {
10654 $this->delegate_connection( $sites_by_action['delegate'] );
10655 }
10656
10657 if ( ! empty( $sites_by_action['skip'] ) ) {
10658 $this->skip_connection( $sites_by_action['skip'] );
10659 }
10660
10661 if ( ! empty( $sites_by_action['allow'] ) ) {
10662 if ( ! $fs->is_registered() || ! $this->_is_network_active ) {
10663 $next_page = $fs->opt_in(
10664 false,
10665 false,
10666 false,
10667 false,
10668 false,
10669 false,
10670 false,
10671 $sites_by_action['allow']
10672 );
10673 } else {
10674 $next_page = $fs->install_with_user(
10675 $this->get_network_user(),
10676 false,
10677 false,
10678 false,
10679 true,
10680 $sites_by_action['allow']
10681 );
10682 }
10683
10684 if ( is_object( $next_page ) && isset( $next_page->error ) ) {
10685 $error = $next_page->error;
10686 }
10687 }
10688 }
10689
10690 if ( empty( $next_page ) ) {
10691 $next_page = $this->get_after_activation_url( 'after_network_activation_url' );
10692 }
10693 } else {
10694 $error = $this->get_text_inline( 'invalid_site_details_collection', 'Invalid site details collection.' );
10695 }
10696
10697 $result = array(
10698 'success' => ( false === $error )
10699 );
10700
10701 if ( false !== $error ) {
10702 $result['error'] = $error;
10703 } else {
10704 $result['next_page'] = $next_page;
10705 }
10706
10707 echo json_encode( $result );
10708
10709 exit;
10710 }
10711
10712 /**
10713 * Billing update AJAX callback.
10714 *
10715 * @author Vova Feldman (@svovaf)
10716 * @since 1.2.1.5
10717 */
10718 function _update_billing_ajax_action() {
10719 $this->_logger->entrance();
10720
10721 $this->check_ajax_referer( 'update_billing' );
10722
10723 if ( ! $this->is_user_admin() ) {
10724 // Only for admins.
10725 self::shoot_ajax_failure();
10726 }
10727
10728 $billing = fs_request_get( 'billing' );
10729
10730 $api = $this->get_api_user_scope();
10731 $result = $api->call( '/billing.json', 'put', array_merge( $billing, array(
10732 'plugin_id' => $this->get_parent_id(),
10733 ) ) );
10734
10735 if ( ! $this->is_api_result_entity( $result ) ) {
10736 self::shoot_ajax_failure();
10737 }
10738
10739 // Purge cached billing.
10740 $this->get_api_user_scope()->purge_cache( 'billing.json' );
10741
10742 self::shoot_ajax_success();
10743 }
10744
10745 /**
10746 * Trial start for anonymous users (AJAX callback).
10747 *
10748 * @author Vova Feldman (@svovaf)
10749 * @since 1.2.1.5
10750 */
10751 function _start_trial_ajax_action() {
10752 $this->_logger->entrance();
10753
10754 $this->check_ajax_referer( 'start_trial' );
10755
10756 if ( ! $this->is_user_admin() ) {
10757 // Only for admins.
10758 self::shoot_ajax_failure();
10759 }
10760
10761 $trial_data = fs_request_get( 'trial' );
10762
10763 $next_page = $this->opt_in(
10764 false,
10765 false,
10766 false,
10767 false,
10768 false,
10769 $trial_data['plan_id']
10770 );
10771
10772 if ( is_object( $next_page ) && $this->is_api_error( $next_page ) ) {
10773 self::shoot_ajax_failure(
10774 isset( $next_page->error ) ?
10775 $next_page->error->message :
10776 var_export( $next_page, true )
10777 );
10778 }
10779
10780 $this->shoot_ajax_success( array(
10781 'next_page' => $next_page,
10782 ) );
10783 }
10784
10785 /**
10786 * @author Leo Fajardo (@leorw)
10787 * @since 1.2.0
10788 */
10789 function _resend_license_key_ajax_action() {
10790 $this->_logger->entrance();
10791
10792 $this->check_ajax_referer( 'resend_license_key' );
10793
10794 $email_address = sanitize_email( trim( fs_request_get( 'email', '', 'post' ) ) );
10795
10796 if ( empty( $email_address ) ) {
10797 exit;
10798 }
10799
10800 $error = false;
10801
10802 $api = $this->get_api_plugin_scope();
10803 $result = $api->call( '/licenses/resend.json', 'post',
10804 array(
10805 'email' => $email_address,
10806 'url' => home_url(),
10807 )
10808 );
10809
10810 if ( is_object( $result ) && isset( $result->error ) ) {
10811 $error = $result->error;
10812
10813 if ( in_array( $error->code, array( 'invalid_email', 'no_user' ) ) ) {
10814 $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' );
10815 } else if ( 'no_license' === $error->code ) {
10816 $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' );
10817 } else {
10818 $error = $error->message;
10819 }
10820 }
10821
10822 $licenses = array(
10823 'success' => ( false === $error )
10824 );
10825
10826 if ( false !== $error ) {
10827 $licenses['error'] = sprintf( '%s... %s', $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ), strtolower( $error ) );
10828 }
10829
10830 echo json_encode( $licenses );
10831
10832 exit;
10833 }
10834
10835 /**
10836 * @author Vova Feldman (@svovaf)
10837 * @since 1.2.1.8
10838 *
10839 * @var string
10840 */
10841 private static $_pagenow;
10842
10843 /**
10844 * Get current page or the referer if executing a WP AJAX request.
10845 *
10846 * @author Vova Feldman (@svovaf)
10847 * @since 1.2.1.8
10848 *
10849 * @return string
10850 */
10851 static function get_current_page() {
10852 if ( ! isset( self::$_pagenow ) ) {
10853 global $pagenow;
10854
10855 self::$_pagenow = $pagenow;
10856
10857 if ( self::is_ajax() &&
10858 'admin-ajax.php' === $pagenow
10859 ) {
10860 $referer = fs_get_raw_referer();
10861
10862 if ( is_string( $referer ) ) {
10863 $parts = explode( '?', $referer );
10864
10865 self::$_pagenow = basename( $parts[0] );
10866 }
10867 }
10868 }
10869
10870 return self::$_pagenow;
10871 }
10872
10873 /**
10874 * Helper method to check if user in the plugins page.
10875 *
10876 * @author Vova Feldman (@svovaf)
10877 * @since 1.2.1.5
10878 *
10879 * @return bool
10880 */
10881 static function is_plugins_page() {
10882 return ( 'plugins.php' === self::get_current_page() );
10883 }
10884
10885 /**
10886 * Helper method to check if user in the themes page.
10887 *
10888 * @author Vova Feldman (@svovaf)
10889 * @since 1.2.2.6
10890 *
10891 * @return bool
10892 */
10893 static function is_themes_page() {
10894 return ( 'themes.php' === self::get_current_page() );
10895 }
10896
10897 #----------------------------------------------------------------------------------
10898 #region Affiliation
10899 #----------------------------------------------------------------------------------
10900
10901 /**
10902 * @author Leo Fajardo (@leorw)
10903 * @since 1.2.3
10904 *
10905 * @return bool
10906 */
10907 function has_affiliate_program() {
10908 if ( ! is_object( $this->_plugin ) ) {
10909 return false;
10910 }
10911
10912 return $this->_plugin->has_affiliate_program();
10913 }
10914
10915 /**
10916 * @author Leo Fajardo (@leorw)
10917 * @since 1.2.4
10918 */
10919 private function fetch_affiliate_terms() {
10920 if ( ! is_object( $this->plugin_affiliate_terms ) ) {
10921 $plugins_api = $this->get_api_plugin_scope();
10922 $affiliate_terms = $plugins_api->get( '/aff.json?type=affiliation', false );
10923
10924 if ( ! $this->is_api_result_entity( $affiliate_terms ) ) {
10925 return;
10926 }
10927
10928 $this->plugin_affiliate_terms = new FS_AffiliateTerms( $affiliate_terms );
10929 }
10930 }
10931
10932 /**
10933 * @author Leo Fajardo (@leorw)
10934 * @since 1.2.4
10935 */
10936 private function fetch_affiliate_and_custom_terms() {
10937 if ( ! empty( $this->_storage->affiliate_application_data ) ) {
10938 $application_data = $this->_storage->affiliate_application_data;
10939 $flush = ( ! isset( $application_data['status'] ) || 'pending' === $application_data['status'] );
10940
10941 $users_api = $this->get_api_user_scope();
10942 $result = $users_api->get( "/plugins/{$this->_plugin->id}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json", $flush );
10943 if ( $this->is_api_result_object( $result, 'affiliates' ) ) {
10944 if ( ! empty( $result->affiliates ) ) {
10945 $affiliate = new FS_Affiliate( $result->affiliates[0] );
10946
10947 if ( ! isset( $application_data['status'] ) || $application_data['status'] !== $affiliate->status ) {
10948 $application_data['status'] = $affiliate->status;
10949 $this->_storage->affiliate_application_data = $application_data;
10950 }
10951
10952 if ( $affiliate->is_using_custom_terms ) {
10953 $affiliate_terms = $users_api->get( "/plugins/{$this->_plugin->id}/affiliates/{$affiliate->id}/aff/{$affiliate->custom_affiliate_terms_id}.json", $flush );
10954 if ( $this->is_api_result_entity( $affiliate_terms ) ) {
10955 $this->custom_affiliate_terms = new FS_AffiliateTerms( $affiliate_terms );
10956 }
10957 }
10958
10959 $this->affiliate = $affiliate;
10960 }
10961 }
10962 }
10963 }
10964
10965 /**
10966 * @author Leo Fajardo (@leorw)
10967 * @since 1.2.3
10968 */
10969 private function fetch_affiliate_and_terms() {
10970 $this->_logger->entrance();
10971
10972 $this->fetch_affiliate_terms();
10973 $this->fetch_affiliate_and_custom_terms();
10974 }
10975
10976 /**
10977 * @author Leo Fajardo (@leorw)
10978 * @since 1.2.3
10979 *
10980 * @return FS_Affiliate
10981 */
10982 function get_affiliate() {
10983 return $this->affiliate;
10984 }
10985
10986
10987 /**
10988 * @author Leo Fajardo (@leorw)
10989 * @since 1.2.3
10990 *
10991 * @return FS_AffiliateTerms
10992 */
10993 function get_affiliate_terms() {
10994 return is_object( $this->custom_affiliate_terms ) ?
10995 $this->custom_affiliate_terms :
10996 $this->plugin_affiliate_terms;
10997 }
10998
10999 /**
11000 * @author Leo Fajardo (@leorw)
11001 * @since 1.2.3
11002 */
11003 function _submit_affiliate_application() {
11004 $this->_logger->entrance();
11005
11006 $this->check_ajax_referer( 'submit_affiliate_application' );
11007
11008 if ( ! $this->is_user_admin() ) {
11009 // Only for admins.
11010 self::shoot_ajax_failure();
11011 }
11012
11013 $affiliate = fs_request_get( 'affiliate' );
11014
11015 if ( empty( $affiliate['promotion_methods'] ) ) {
11016 unset( $affiliate['promotion_methods'] );
11017 }
11018
11019 if ( ! empty( $affiliate['additional_domains'] ) ) {
11020 $affiliate['additional_domains'] = array_unique( $affiliate['additional_domains'] );
11021 }
11022
11023 if ( ! $this->is_registered() ) {
11024 // Opt in but don't track usage.
11025 $next_page = $this->opt_in(
11026 false,
11027 false,
11028 false,
11029 false,
11030 false,
11031 false,
11032 true
11033 );
11034
11035 if ( is_object( $next_page ) && $this->is_api_error( $next_page ) ) {
11036 self::shoot_ajax_failure(
11037 isset( $next_page->error ) ?
11038 $next_page->error->message :
11039 var_export( $next_page, true )
11040 );
11041 } else if ( $this->is_pending_activation() ) {
11042 self::shoot_ajax_failure( $this->get_text_inline( 'Account is pending activation.', 'account-is-pending-activation' ) );
11043 }
11044 }
11045
11046 $this->fetch_affiliate_terms();
11047
11048 $api = $this->get_api_user_scope();
11049 $result = $api->call(
11050 ( "/plugins/{$this->_plugin->id}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json" ),
11051 'post',
11052 $affiliate
11053 );
11054
11055 if ( $this->is_api_error( $result ) ) {
11056 self::shoot_ajax_failure(
11057 isset( $result->error ) ?
11058 $result->error->message :
11059 var_export( $result, true )
11060 );
11061 } else {
11062 if ( $this->_admin_notices->has_sticky( 'affiliate_program' ) ) {
11063 $this->_admin_notices->remove_sticky( 'affiliate_program' );
11064 }
11065
11066 $affiliate_application_data = array(
11067 'status' => 'pending',
11068 'stats_description' => $affiliate['stats_description'],
11069 'promotion_method_description' => $affiliate['promotion_method_description'],
11070 );
11071
11072 if ( ! empty( $affiliate['promotion_methods'] ) ) {
11073 $affiliate_application_data['promotion_methods'] = $affiliate['promotion_methods'];
11074 }
11075
11076 if ( ! empty( $affiliate['domain'] ) ) {
11077 $affiliate_application_data['domain'] = $affiliate['domain'];
11078 }
11079
11080 if ( ! empty( $affiliate['additional_domains'] ) ) {
11081 $affiliate_application_data['additional_domains'] = $affiliate['additional_domains'];
11082 }
11083
11084 $this->_storage->affiliate_application_data = $affiliate_application_data;
11085 }
11086
11087 // Purge cached affiliate.
11088 $api->purge_cache( 'affiliate.json' );
11089
11090 self::shoot_ajax_success( $result );
11091 }
11092
11093 /**
11094 * @author Leo Fajardo (@leorw)
11095 * @since 1.2.3
11096 *
11097 * @return array|null
11098 */
11099 function get_affiliate_application_data() {
11100 if ( empty( $this->_storage->affiliate_application_data ) ) {
11101 return null;
11102 }
11103
11104 return $this->_storage->affiliate_application_data;
11105 }
11106
11107 #endregion Affiliation ------------------------------------------------------------
11108
11109 #----------------------------------------------------------------------------------
11110 #region URL Generators
11111 #----------------------------------------------------------------------------------
11112
11113 /**
11114 * Alias to pricing_url().
11115 *
11116 * @author Vova Feldman (@svovaf)
11117 * @since 1.0.2
11118 *
11119 * @uses pricing_url()
11120 *
11121 * @param string $period Billing cycle
11122 * @param bool $is_trial
11123 *
11124 * @return string
11125 */
11126 function get_upgrade_url( $period = WP_FS__PERIOD_ANNUALLY, $is_trial = false ) {
11127 return $this->pricing_url( $period, $is_trial );
11128 }
11129
11130 /**
11131 * @author Vova Feldman (@svovaf)
11132 * @since 1.0.9
11133 *
11134 * @uses get_upgrade_url()
11135 *
11136 * @return string
11137 */
11138 function get_trial_url() {
11139 return $this->get_upgrade_url( WP_FS__PERIOD_ANNUALLY, true );
11140 }
11141
11142 /**
11143 * Plugin's pricing URL.
11144 *
11145 * @author Vova Feldman (@svovaf)
11146 * @since 1.0.4
11147 *
11148 * @param string $billing_cycle Billing cycle
11149 *
11150 * @param bool $is_trial
11151 *
11152 * @return string
11153 */
11154 function pricing_url( $billing_cycle = WP_FS__PERIOD_ANNUALLY, $is_trial = false ) {
11155 $this->_logger->entrance();
11156
11157 $params = array(
11158 'billing_cycle' => $billing_cycle
11159 );
11160
11161 if ( $is_trial ) {
11162 $params['trial'] = 'true';
11163 }
11164
11165 if ( $this->is_addon() ) {
11166 return $this->_parent->addon_url( $this->_slug );
11167 }
11168
11169 return $this->_get_admin_page_url( 'pricing', $params );
11170 }
11171
11172 /**
11173 * Checkout page URL.
11174 *
11175 * @author Vova Feldman (@svovaf)
11176 * @since 1.0.6
11177 *
11178 * @param string $billing_cycle Billing cycle
11179 * @param bool $is_trial
11180 * @param array $extra (optional) Extra parameters, override other query params.
11181 *
11182 * @return string
11183 */
11184 function checkout_url(
11185 $billing_cycle = WP_FS__PERIOD_ANNUALLY,
11186 $is_trial = false,
11187 $extra = array()
11188 ) {
11189 $this->_logger->entrance();
11190
11191 $params = array(
11192 'checkout' => 'true',
11193 'billing_cycle' => $billing_cycle,
11194 );
11195
11196 if ( $is_trial ) {
11197 $params['trial'] = 'true';
11198 }
11199
11200 /**
11201 * Params in extra override other params.
11202 */
11203 $params = array_merge( $params, $extra );
11204
11205 return $this->_get_admin_page_url( 'pricing', $params );
11206 }
11207
11208 /**
11209 * Add-on checkout URL.
11210 *
11211 * @author Vova Feldman (@svovaf)
11212 * @since 1.1.7
11213 *
11214 * @param number $addon_id
11215 * @param number $pricing_id
11216 * @param string $billing_cycle
11217 * @param bool $is_trial
11218 *
11219 * @return string
11220 */
11221 function addon_checkout_url(
11222 $addon_id,
11223 $pricing_id,
11224 $billing_cycle = WP_FS__PERIOD_ANNUALLY,
11225 $is_trial = false
11226 ) {
11227 return $this->checkout_url( $billing_cycle, $is_trial, array(
11228 'plugin_id' => $addon_id,
11229 'pricing_id' => $pricing_id,
11230 ) );
11231 }
11232
11233 #endregion
11234
11235 #endregion ------------------------------------------------------------------
11236
11237 /**
11238 * Check if plugin has any add-ons.
11239 *
11240 * @author Vova Feldman (@svovaf)
11241 * @since 1.0.5
11242 *
11243 * @since 1.1.7.3 Base logic only on the parameter provided by the developer in the init function.
11244 *
11245 * @return bool
11246 */
11247 function has_addons() {
11248 $this->_logger->entrance();
11249
11250 return $this->_has_addons;
11251 }
11252
11253 /**
11254 * Check if plugin can work in anonymous mode.
11255 *
11256 * @author Vova Feldman (@svovaf)
11257 * @since 1.0.9
11258 *
11259 * @return bool
11260 *
11261 * @deprecated Please use is_enable_anonymous() instead.
11262 */
11263 function enable_anonymous() {
11264 return $this->_enable_anonymous;
11265 }
11266
11267 /**
11268 * Check if plugin can work in anonymous mode.
11269 *
11270 * @author Vova Feldman (@svovaf)
11271 * @since 1.1.9
11272 *
11273 * @return bool
11274 */
11275 function is_enable_anonymous() {
11276 return $this->_enable_anonymous;
11277 }
11278
11279 /**
11280 * Check if plugin is premium only (no free plans).
11281 *
11282 * @author Vova Feldman (@svovaf)
11283 * @since 1.1.9
11284 *
11285 * @return bool
11286 */
11287 function is_only_premium() {
11288 return $this->_is_premium_only;
11289 }
11290
11291 /**
11292 * Checks if the plugin's type is "plugin". The other type is "theme".
11293 *
11294 * @author Leo Fajardo (@leorw)
11295 * @since 1.2.2
11296 *
11297 * @return bool
11298 */
11299 function is_plugin() {
11300 return ( WP_FS__MODULE_TYPE_PLUGIN === $this->_module_type );
11301 }
11302
11303 /**
11304 * @author Leo Fajardo (@leorw)
11305 * @since 1.2.2
11306 *
11307 * @return string
11308 */
11309 function get_module_type() {
11310 if ( ! isset( $this->_module_type ) ) {
11311 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
11312 $this->_module_type = $id_slug_type_path_map[ $this->_module_id ]['type'];
11313 }
11314
11315 return $this->_module_type;
11316 }
11317
11318 /**
11319 * @author Leo Fajardo (@leorw)
11320 * @since 1.2.2
11321 *
11322 * @return string
11323 */
11324 function get_plugin_main_file_path() {
11325 return $this->_plugin_main_file_path;
11326 }
11327
11328 /**
11329 * Check if module has a premium code version.
11330 *
11331 * Serviceware module might be freemium without any
11332 * premium code version, where the paid features
11333 * are all part of the service.
11334 *
11335 * @author Vova Feldman (@svovaf)
11336 * @since 1.2.1.6
11337 *
11338 * @return bool
11339 */
11340 function has_premium_version() {
11341 return $this->_has_premium_version;
11342 }
11343
11344 /**
11345 * Check if feature supported with current site's plan.
11346 *
11347 * @author Vova Feldman (@svovaf)
11348 * @since 1.0.1
11349 *
11350 * @todo IMPLEMENT
11351 *
11352 * @param number $feature_id
11353 *
11354 * @throws Exception
11355 */
11356 function is_feature_supported( $feature_id ) {
11357 throw new Exception( 'not implemented' );
11358 }
11359
11360 /**
11361 * @author Vova Feldman (@svovaf)
11362 * @since 1.0.1
11363 *
11364 * @return bool Is running in SSL/HTTPS
11365 */
11366 function is_ssl() {
11367 return WP_FS__IS_HTTPS;
11368 }
11369
11370 /**
11371 * @author Vova Feldman (@svovaf)
11372 * @since 1.0.9
11373 *
11374 * @return bool Is running in AJAX call.
11375 *
11376 * @link http://wordpress.stackexchange.com/questions/70676/how-to-check-if-i-am-in-admin-ajax
11377 */
11378 static function is_ajax() {
11379 return ( defined( 'DOING_AJAX' ) && DOING_AJAX );
11380 }
11381
11382 /**
11383 * Check if it's an AJAX call targeted for the current module.
11384 *
11385 * @author Vova Feldman (@svovaf)
11386 * @since 1.2.0
11387 *
11388 * @param array|string $actions Collection of AJAX actions.
11389 *
11390 * @return bool
11391 */
11392 function is_ajax_action( $actions ) {
11393 // Verify it's an ajax call.
11394 if ( ! self::is_ajax() ) {
11395 return false;
11396 }
11397
11398 // Verify the call is relevant for the plugin.
11399 if ( $this->_module_id != fs_request_get( 'module_id' ) ) {
11400 return false;
11401 }
11402
11403 // Verify it's one of the specified actions.
11404 if ( is_string( $actions ) ) {
11405 $actions = explode( ',', $actions );
11406 }
11407
11408 if ( is_array( $actions ) && 0 < count( $actions ) ) {
11409 $ajax_action = fs_request_get( 'action' );
11410
11411 foreach ( $actions as $action ) {
11412 if ( $ajax_action === $this->get_action_tag( $action ) ) {
11413 return true;
11414 }
11415 }
11416 }
11417
11418 return false;
11419 }
11420
11421 /**
11422 * Check if it's an AJAX call targeted for current request.
11423 *
11424 * @author Vova Feldman (@svovaf)
11425 * @since 1.2.0
11426 *
11427 * @param array|string $actions Collection of AJAX actions.
11428 * @param number|null $module_id
11429 *
11430 * @return bool
11431 */
11432 static function is_ajax_action_static( $actions, $module_id = null ) {
11433 // Verify it's an ajax call.
11434 if ( ! self::is_ajax() ) {
11435 return false;
11436 }
11437
11438
11439 if ( ! empty( $module_id ) ) {
11440 // Verify the call is relevant for the plugin.
11441 if ( $module_id != fs_request_get( 'module_id' ) ) {
11442 return false;
11443 }
11444 }
11445
11446 // Verify it's one of the specified actions.
11447 if ( is_string( $actions ) ) {
11448 $actions = explode( ',', $actions );
11449 }
11450
11451 if ( is_array( $actions ) && 0 < count( $actions ) ) {
11452 $ajax_action = fs_request_get( 'action' );
11453
11454 foreach ( $actions as $action ) {
11455 if ( $ajax_action === self::get_ajax_action_static( $action, $module_id ) ) {
11456 return true;
11457 }
11458 }
11459 }
11460
11461 return false;
11462 }
11463
11464 /**
11465 * @author Vova Feldman (@svovaf)
11466 * @since 1.1.7
11467 *
11468 * @return bool
11469 */
11470 static function is_cron() {
11471 return ( defined( 'DOING_CRON' ) && DOING_CRON );
11472 }
11473
11474 /**
11475 * Check if a real user is visiting the admin dashboard.
11476 *
11477 * @author Vova Feldman (@svovaf)
11478 * @since 1.1.7
11479 *
11480 * @return bool
11481 */
11482 function is_user_in_admin() {
11483 return is_admin() && ! self::is_ajax() && ! self::is_cron();
11484 }
11485
11486 /**
11487 * Check if a real user is in the customizer view.
11488 *
11489 * @author Vova Feldman (@svovaf)
11490 * @since 1.2.2.7
11491 *
11492 * @return bool
11493 */
11494 static function is_customizer() {
11495 return is_customize_preview();
11496 }
11497
11498 /**
11499 * Check if running in HTTPS and if site's plan matching the specified plan.
11500 *
11501 * @param string $plan
11502 * @param bool $exact
11503 *
11504 * @return bool
11505 */
11506 function is_ssl_and_plan( $plan, $exact = false ) {
11507 return ( $this->is_ssl() && $this->is_plan( $plan, $exact ) );
11508 }
11509
11510 /**
11511 * Construct plugin's settings page URL.
11512 *
11513 * @author Vova Feldman (@svovaf)
11514 * @since 1.0.4
11515 *
11516 * @param string $page
11517 * @param array $params
11518 * @param bool|null $network
11519 *
11520 * @return string
11521 */
11522 function _get_admin_page_url( $page = '', $params = array(), $network = null ) {
11523 if ( is_null( $network ) ) {
11524 $network = (
11525 $this->_is_network_active &&
11526 ( fs_is_network_admin() || ! $this->is_delegated_connection() )
11527 );
11528 }
11529
11530 if ( 0 < count( $params ) ) {
11531 foreach ( $params as $k => $v ) {
11532 $params[ $k ] = urlencode( $v );
11533 }
11534 }
11535
11536 $page_param = $this->_menu->get_slug( $page );
11537
11538 if ( empty( $page ) &&
11539 $this->is_theme() &&
11540 // Show the opt-in as an overlay for free wp.org themes or themes without any settings page.
11541 ( $this->is_free_wp_org_theme() || ! $this->has_settings_menu() )
11542 ) {
11543 $params[ $this->get_unique_affix() . '_show_optin' ] = 'true';
11544
11545 return add_query_arg(
11546 $params,
11547 $this->admin_url( 'themes.php', 'admin', $network )
11548 );
11549 }
11550
11551 if ( ! $this->has_settings_menu() ) {
11552 if ( ! empty( $page ) ) {
11553 // Module doesn't have a setting page, but since the request is for
11554 // a specific Freemius page, use the admin.php path.
11555 return add_query_arg( array_merge( $params, array(
11556 'page' => $page_param,
11557 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
11558 } else {
11559 if ( $this->is_activation_mode() ) {
11560 /**
11561 * @author Vova Feldman
11562 * @since 1.2.1.6
11563 *
11564 * If plugin doesn't have a settings page, create one for the opt-in screen.
11565 */
11566 return add_query_arg( array_merge( $params, array(
11567 'page' => $this->_slug,
11568 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
11569 } else {
11570 // Plugin without a settings page.
11571 return add_query_arg(
11572 $params,
11573 $this->admin_url( 'plugins.php', 'admin', $network )
11574 );
11575 }
11576 }
11577 }
11578
11579 // Module has a submenu settings page.
11580 if ( ! $this->_menu->is_top_level() ) {
11581 $parent_slug = $this->_menu->get_parent_slug();
11582 $menu_file = ( false !== strpos( $parent_slug, '.php' ) ) ?
11583 $parent_slug :
11584 'admin.php';
11585
11586 return add_query_arg( array_merge( $params, array(
11587 'page' => $page_param,
11588 ) ), $this->admin_url( $menu_file, 'admin', $network ) );
11589 }
11590
11591 // Module has a top level CPT settings page.
11592 if ( $this->_menu->is_cpt() ) {
11593 if ( empty( $page ) && $this->is_activation_mode() ) {
11594 return add_query_arg( array_merge( $params, array(
11595 'page' => $page_param
11596 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
11597 } else {
11598 if ( ! empty( $page ) ) {
11599 $params['page'] = $page_param;
11600 }
11601
11602 return add_query_arg(
11603 $params,
11604 $this->admin_url( $this->_menu->get_raw_slug(), 'admin', $network )
11605 );
11606 }
11607 }
11608
11609 // Module has a custom top level settings page.
11610 return add_query_arg( array_merge( $params, array(
11611 'page' => $page_param,
11612 ) ), $this->admin_url( 'admin.php', 'admin', $network ) );
11613 }
11614
11615 #--------------------------------------------------------------------------------
11616 #region Multisite
11617 #--------------------------------------------------------------------------------
11618
11619 /**
11620 * @author Leo Fajardo (@leorw)
11621 * @since 2.0.0
11622 *
11623 * @return bool
11624 */
11625 function is_network_active() {
11626 return $this->_is_network_active;
11627 }
11628
11629 /**
11630 * Delegate activation for the given sites in the network (or all sites if `null`) to site admins.
11631 *
11632 * @author Leo Fajardo (@leorw)
11633 * @since 2.0.0
11634 *
11635 * @param array|null $sites
11636 */
11637 private function delegate_connection( $sites = null ) {
11638 $this->_logger->entrance();
11639
11640 $this->_admin_notices->remove_sticky( 'connect_account' );
11641
11642 if ( is_null( $sites ) ) {
11643 // All sites delegation.
11644 $this->_storage->store( 'is_delegated_connection', true, true, true );
11645 } else {
11646 // Specified sites delegation.
11647 foreach ( $sites as $site ) {
11648 $this->delegate_site_connection( $site['blog_id'] );
11649 }
11650 }
11651
11652 $this->network_upgrade_mode_completed();
11653 }
11654
11655 /**
11656 * Delegate specific network site conncetion to the site admin.
11657 *
11658 * @author Vova Feldman (@svovaf)
11659 * @since 2.0.0
11660 *
11661 * @param int $blog_id
11662 */
11663 private function delegate_site_connection( $blog_id ) {
11664 $this->_storage->store( 'is_delegated_connection', true, $blog_id, true );
11665 }
11666
11667 /**
11668 * Check if super-admin delegated the connection of ALL sites to the site admins.
11669 *
11670 * @author Vova Feldman (@svovaf)
11671 * @since 2.0.0
11672 *
11673 * @return bool
11674 */
11675 function is_network_delegated_connection() {
11676 if ( ! $this->_is_network_active ) {
11677 return false;
11678 }
11679
11680 return $this->_storage->get( 'is_delegated_connection', false, true );
11681 }
11682
11683 /**
11684 * @author Leo Fajardo (@leorw)
11685 * @since 2.0.0
11686 *
11687 * @param int $blog_id
11688 *
11689 * @return bool
11690 */
11691 function is_site_delegated_connection( $blog_id = 0 ) {
11692 if ( ! $this->_is_network_active ) {
11693 return false;
11694 }
11695
11696 if ( 0 == $blog_id ) {
11697 $blog_id = get_current_blog_id();
11698 }
11699
11700 return $this->_storage->get( 'is_delegated_connection', false, $blog_id );
11701 }
11702
11703 /**
11704 * Check if delegated the connection. When running within the the network admin,
11705 * and haven't specified the blog ID, checks if network level delegated. If running
11706 * within a site admin or specified a blog ID, check if delegated the connection for
11707 * the current context site.
11708 *
11709 * If executed outside the the admin, check if delegated the connection
11710 * for the current context site OR the whole network.
11711 *
11712 * @author Vova Feldman (@svovaf)
11713 * @since 2.0.0
11714 *
11715 * @param int $blog_id If set, checks if network delegated or blog specific delegated.
11716 *
11717 * @return bool
11718 */
11719 function is_delegated_connection( $blog_id = 0 ) {
11720 if ( ! $this->_is_network_active ) {
11721 return false;
11722 }
11723
11724 if ( fs_is_network_admin() && 0 == $blog_id ) {
11725 return $this->is_network_delegated_connection();
11726 }
11727
11728 return (
11729 $this->is_network_delegated_connection() ||
11730 $this->is_site_delegated_connection( $blog_id )
11731 );
11732 }
11733
11734 /**
11735 * Check if the current module is active for the site.
11736 *
11737 * @author Vova Feldman (@svovaf)
11738 * @since 2.0.0
11739 *
11740 * @param int $blog_id
11741 *
11742 * @return bool
11743 */
11744 function is_active_for_site( $blog_id ) {
11745 if ( ! is_multisite() ) {
11746 // Not a multisite and this code is executed, means that the plugin is active.
11747 return true;
11748 }
11749
11750 if ( $this->is_theme() ) {
11751 // All themes are site level activated.
11752 return true;
11753 }
11754
11755 if ( $this->_is_network_active ) {
11756 // Plugin was network activated so it's active.
11757 return true;
11758 }
11759
11760 return in_array( $this->_plugin_basename, (array) get_blog_option( $blog_id, 'active_plugins', array() ) );
11761 }
11762
11763 /**
11764 * @author Leo Fajardo (@leorw)
11765 * @since 2.0.0
11766 *
11767 * @return array Active & public sites collection.
11768 */
11769 static function get_sites() {
11770 /**
11771 * For consistency with get_blog_list() which only return active public sites.
11772 *
11773 * @author Vova Feldman (@svovaf)
11774 */
11775 $args = array(
11776 'public' => 1,
11777 'archived' => 0,
11778 'mature' => 0,
11779 'spam' => 0,
11780 'deleted' => 0,
11781 );
11782
11783 if ( function_exists( 'get_sites' ) ) {
11784 // For WP 4.6 and above.
11785 return get_sites( $args );
11786 } else if ( function_exists( 'wp_get_sites' ) ) {
11787 // For WP 3.7 to WP 4.5.
11788 return wp_get_sites( $args );
11789 } else {
11790 // For WP 3.6 and below.
11791 return get_blog_list( 0, 'all' );
11792 }
11793 }
11794
11795 /**
11796 * Checks if a given blog is active.
11797 *
11798 * @author Vova Feldman (@svovaf)
11799 * @since 2.0.0
11800 *
11801 * @param $blog_id
11802 *
11803 * @return bool
11804 */
11805 private static function is_site_active( $blog_id ) {
11806 global $wpdb;
11807
11808 $blog_info = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->blogs} WHERE blog_id = %d", $blog_id ) );
11809
11810 if ( ! is_object( $blog_info ) ) {
11811 return false;
11812 }
11813
11814 return (
11815 true == $blog_info->public &&
11816 false == $blog_info->archived &&
11817 false == $blog_info->mature &&
11818 false == $blog_info->spam &&
11819 false == $blog_info->deleted
11820 );
11821 }
11822
11823 /**
11824 * Get a mapping between the site addresses to their blog IDs.
11825 *
11826 * @author Vova Feldman (@svovaf)
11827 * @since 2.0.0
11828 *
11829 * @return array {
11830 * @key string Site address without protocol with a trailing slash.
11831 * @value int Site's blog ID.
11832 * }
11833 */
11834 private function get_address_to_blog_map() {
11835 $sites = self::get_sites();
11836
11837 // Map site addresses to their blog IDs.
11838 $address_to_blog_map = array();
11839 foreach ( $sites as $site ) {
11840 $blog_id = self::get_site_blog_id( $site );
11841 $address = trailingslashit( fs_strip_url_protocol( get_site_url( $blog_id ) ) );
11842 $address_to_blog_map[ $address ] = $blog_id;
11843 }
11844
11845 return $address_to_blog_map;
11846 }
11847
11848 /**
11849 * Get a mapping between the site addresses to their blog IDs.
11850 *
11851 * @author Vova Feldman (@svovaf)
11852 * @since 2.0.0
11853 *
11854 * @return array {
11855 * @key int Site's blog ID.
11856 * @value FS_Site Associated install.
11857 * }
11858 */
11859 function get_blog_install_map() {
11860 $sites = self::get_sites();
11861
11862 // Map site blog ID to its install.
11863 $install_map = array();
11864
11865 foreach ( $sites as $site ) {
11866 $blog_id = self::get_site_blog_id( $site );
11867 $install = $this->get_install_by_blog_id( $blog_id );
11868
11869 if ( is_object( $install ) ) {
11870 $install_map[ $blog_id ] = $install;
11871 }
11872 }
11873
11874 return $install_map;
11875 }
11876
11877 /**
11878 * @author Leo Fajardo (@leorw)
11879 *
11880 * @return null|array {
11881 * 'install' => FS_Site Module's install,
11882 * 'blog_id' => string The associated blog ID.
11883 * }
11884 */
11885 private function find_first_install() {
11886 $sites = self::get_sites();
11887
11888 foreach ( $sites as $site ) {
11889 $blog_id = self::get_site_blog_id( $site );
11890 $install = $this->get_install_by_blog_id( $blog_id );
11891
11892 if ( is_object( $install ) ) {
11893 return array(
11894 'install' => $install,
11895 'blog_id' => $blog_id
11896 );
11897 }
11898 }
11899
11900 return null;
11901 }
11902
11903 /**
11904 * Switches the Freemius site level context to a specified blog.
11905 *
11906 * @author Vova Feldman (@svovaf)
11907 * @since 2.0.0
11908 *
11909 * @param int $blog_id
11910 * @param FS_Site $install
11911 */
11912 function switch_to_blog( $blog_id, FS_Site $install = null ) {
11913 if ( $blog_id == $this->_context_is_network_or_blog_id ) {
11914 return;
11915 }
11916
11917 switch_to_blog( $blog_id );
11918 $this->_context_is_network_or_blog_id = $blog_id;
11919
11920 self::$_accounts->set_site_blog_context( $blog_id );
11921 $this->_storage->set_site_blog_context( $blog_id );
11922 $this->_storage->set_network_active( true, $this->is_delegated_connection( $blog_id ) );
11923
11924 $this->_site = is_object( $install ) ?
11925 $install :
11926 $this->get_install_by_blog_id( $blog_id );
11927
11928 $this->_user = false;
11929 $this->_licenses = false;
11930 $this->_license = null;
11931
11932 if ( is_object( $this->_site ) ) {
11933 // Try to fetch user from install.
11934 $this->_user = self::_get_user_by_id( $this->_site->user_id );
11935
11936 if ( ! is_object( $this->_user ) &&
11937 FS_User::is_valid_id( $this->_storage->prev_user_id )
11938 ) {
11939 // Try to fetch previously saved user.
11940 $this->_user = self::_get_user_by_id( $this->_storage->prev_user_id );
11941
11942 if ( ! is_object( $this->_user ) ) {
11943 // Fallback to network's user.
11944 $this->_user = $this->get_network_user();
11945 }
11946 }
11947
11948 $all_plugin_licenses = self::get_all_licenses( $this->_module_id );
11949
11950 if ( ! empty( $all_plugin_licenses ) ) {
11951 if ( ! FS_Plugin_License::is_valid_id( $this->_site->license_id ) ) {
11952 $this->_license = null;
11953 } else {
11954 $license_found = false;
11955 foreach ( $all_plugin_licenses as $license ) {
11956 if ( $license->id == $this->_site->license_id ) {
11957 // License found.
11958 $this->_license = $license;
11959 $license_found = true;
11960 break;
11961 }
11962 }
11963
11964 if ( $license_found ) {
11965 $this->link_license_2_user( $this->_license->id, $this->_user->id );
11966 }
11967 }
11968
11969 $this->_licenses = $this->get_user_licenses( $this->_user->id );
11970 }
11971 }
11972
11973 unset( $this->_site_api );
11974 unset( $this->_user_api );
11975 }
11976
11977 /**
11978 * Restore the blog context to the blog that originally loaded the module.
11979 *
11980 * @author Vova Feldman (@svovaf)
11981 * @since 2.0.0
11982 */
11983 function restore_current_blog() {
11984 $this->switch_to_blog( $this->_blog_id );
11985 }
11986
11987 /**
11988 * @author Vova Feldman (@svovaf)
11989 * @since 2.0.0
11990 *
11991 * @param array|WP_Site $site
11992 *
11993 * @return int
11994 */
11995 static function get_site_blog_id( &$site ) {
11996 return ( $site instanceof WP_Site ) ?
11997 $site->blog_id :
11998 $site['blog_id'];
11999 }
12000
12001 /**
12002 * @author Leo Fajardo (@leorw)
12003 * @since 2.0.0
12004 *
12005 * @param array|WP_Site|null $site
12006 *
12007 * @return array
12008 */
12009 function get_site_info( $site = null ) {
12010 $this->_logger->entrance();
12011
12012 $switched = false;
12013
12014 if ( is_null( $site ) ) {
12015 $url = get_site_url();
12016 $name = get_bloginfo( 'name' );
12017 $blog_id = null;
12018 } else {
12019 $blog_id = self::get_site_blog_id( $site );
12020
12021 if ( get_current_blog_id() != $blog_id ) {
12022 switch_to_blog( $blog_id );
12023 $switched = true;
12024 }
12025
12026 if ( $site instanceof WP_Site ) {
12027 $url = $site->siteurl;
12028 $name = $site->blogname;
12029 } else {
12030 $url = get_site_url( $blog_id );
12031 $name = get_bloginfo( 'name' );
12032 }
12033 }
12034
12035 $info = array(
12036 'uid' => $this->get_anonymous_id( $blog_id ),
12037 'url' => $url,
12038 'title' => $name,
12039 'language' => get_bloginfo( 'language' ),
12040 'charset' => get_bloginfo( 'charset' ),
12041 );
12042
12043 if ( is_numeric( $blog_id ) ) {
12044 $info['blog_id'] = $blog_id;
12045 }
12046
12047 if ( $switched ) {
12048 restore_current_blog();
12049 }
12050
12051 return $info;
12052 }
12053
12054 /**
12055 * Load the module's install based on the blog ID.
12056 *
12057 * @author Vova Feldman (@svovaf)
12058 * @since 2.0.0
12059 *
12060 * @param int|null $blog_id
12061 *
12062 * @return FS_Site
12063 */
12064 function get_install_by_blog_id( $blog_id = null ) {
12065 $installs = self::get_all_sites( $this->_module_type, $blog_id );
12066 $install = isset( $installs[ $this->_slug ] ) ? $installs[ $this->_slug ] : null;
12067
12068 if ( is_object( $install ) &&
12069 is_numeric( $install->id ) &&
12070 is_numeric( $install->user_id ) &&
12071 FS_Plugin_Plan::is_valid_id( $install->plan_id )
12072 ) {
12073 // Load site.
12074 $install = clone $install;
12075 }
12076
12077 return $install;
12078 }
12079
12080 /**
12081 * Check if module is installed on a specified site.
12082 *
12083 * @author Vova Feldman (@svovaf)
12084 * @since 2.0.0
12085 *
12086 * @param int|null $blog_id
12087 *
12088 * @return bool
12089 */
12090 function is_installed_on_site( $blog_id = null ) {
12091 $installs = self::get_all_sites( $this->_module_type, $blog_id );
12092 $install = isset( $installs[ $this->_slug ] ) ? $installs[ $this->_slug ] : null;
12093
12094 return (
12095 is_object( $install ) &&
12096 is_numeric( $install->id ) &&
12097 is_numeric( $install->user_id ) &&
12098 FS_Plugin_Plan::is_valid_id( $install->plan_id )
12099 );
12100 }
12101
12102 /**
12103 * Check if super-admin connected at least one site via the network opt-in.
12104 *
12105 * @author Vova Feldman (@svovaf)
12106 * @since 2.0.0
12107 *
12108 * @return bool
12109 */
12110 function is_network_registered() {
12111 if ( ! $this->_is_network_active ) {
12112 return false;
12113 }
12114
12115 return FS_User::is_valid_id( $this->_storage->network_user_id );
12116 }
12117
12118 /**
12119 * Returns the main user associated with the network.
12120 *
12121 * @author Vova Feldman (@svovaf)
12122 * @since 2.0.0
12123 *
12124 * @return FS_User
12125 */
12126 function get_network_user() {
12127 if ( ! $this->_is_network_active ) {
12128 return null;
12129 }
12130
12131 return FS_User::is_valid_id( $this->_storage->network_user_id ) ?
12132 self::_get_user_by_id( $this->_storage->network_user_id ) :
12133 null;
12134 }
12135
12136 /**
12137 * Returns the current context user or the network's main user.
12138 *
12139 * @author Vova Feldman (@svovaf)
12140 * @since 2.0.0
12141 *
12142 * @return FS_User
12143 */
12144 function get_current_or_network_user() {
12145 return ( $this->_user instanceof FS_User ) ?
12146 $this->_user :
12147 $this->get_network_user();
12148 }
12149
12150 /**
12151 * Returns the main install associated with the network.
12152 *
12153 * @author Vova Feldman (@svovaf)
12154 * @since 2.0.0
12155 *
12156 * @return FS_Site
12157 */
12158 function get_network_install() {
12159 if ( ! $this->_is_network_active ) {
12160 return null;
12161 }
12162
12163 return FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ?
12164 $this->get_install_by_blog_id( $this->_storage->network_install_blog_id ) :
12165 null;
12166 }
12167
12168 /**
12169 * Returns the blog ID that is associated with the main install.
12170 *
12171 * @author Leo Fajardo (@leorw)
12172 * @since 2.0.0
12173 *
12174 * @return int|null
12175 */
12176 function get_network_install_blog_id() {
12177 if ( ! $this->_is_network_active ) {
12178 return null;
12179 }
12180
12181 return FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ?
12182 $this->_storage->network_install_blog_id :
12183 null;
12184 }
12185
12186 /**
12187 * Returns the current context install or the network's main install.
12188 *
12189 * @author Vova Feldman (@svovaf)
12190 * @since 2.0.0
12191 *
12192 * @return FS_Site
12193 */
12194 function get_current_or_network_install() {
12195 return ( $this->_site instanceof FS_Site ) ?
12196 $this->_site :
12197 $this->get_network_install();
12198 }
12199
12200 /**
12201 * Check if executing a site level action from the network level admin.
12202 *
12203 * @author Vova Feldman (@svovaf)
12204 * @since 2.0.0
12205 *
12206 * @return false|int If yes, return the requested blog ID.
12207 */
12208 private function is_network_level_site_specific_action() {
12209 if ( ! $this->_is_network_active ) {
12210 return false;
12211 }
12212
12213 if ( ! fs_is_network_admin() ) {
12214 return false;
12215 }
12216
12217 $blog_id = fs_request_get( 'blog_id', '' );
12218
12219 return is_numeric( $blog_id ) ? $blog_id : false;
12220 }
12221
12222 /**
12223 * Check if executing an action from the network level admin.
12224 *
12225 * @author Vova Feldman (@svovaf)
12226 * @since 2.0.0
12227 *
12228 * @return bool
12229 */
12230 private function is_network_level_action() {
12231 return ( $this->_is_network_active && fs_is_network_admin() );
12232 }
12233
12234 /**
12235 * Needs to be executed after site deactivation, archive, deletion, or flag as spam.
12236 * 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.
12237 *
12238 * @author Vova Feldman (@svovaf)
12239 * @since 2.0.0
12240 *
12241 * @param int $context_blog_id
12242 */
12243 private function update_multisite_data_after_site_deactivation( $context_blog_id = 0 ) {
12244 $this->_logger->entrance();
12245
12246 if ( $this->_is_network_active ) {
12247 if ( $context_blog_id == $this->_storage->network_install_blog_id ) {
12248 $installs_map = $this->get_blog_install_map();
12249
12250 foreach ( $installs_map as $blog_id => $install ) {
12251 /**
12252 * @var FS_Site $install
12253 */
12254 if ( $context_blog_id == $blog_id ) {
12255 continue;
12256 }
12257
12258 if ( $install->user_id != $this->_storage->network_user_id ) {
12259 continue;
12260 }
12261
12262 // Switch reference to a blog that is opted-in and belong to the same super-admin.
12263 $this->_storage->network_install_blog_id = $blog_id;
12264 break;
12265 }
12266 }
12267 }
12268
12269 if ( $this->is_sync_cron_scheduled() &&
12270 $context_blog_id == $this->get_sync_cron_blog_id()
12271 ) {
12272 $this->schedule_sync_cron( WP_FS__SCRIPT_START_TIME, true, $context_blog_id );
12273 }
12274
12275 if ( $this->is_install_sync_scheduled() &&
12276 $context_blog_id == $this->get_install_sync_cron_blog_id()
12277 ) {
12278 $this->schedule_install_sync( $context_blog_id );
12279 }
12280 }
12281
12282 /**
12283 * Executed after site deactivation, archive, or flag as spam.
12284 *
12285 * @author Vova Feldman (@svovaf)
12286 * @since 2.0.0
12287 *
12288 * @param int $context_blog_id
12289 */
12290 public function _after_site_deactivated_callback( $context_blog_id = 0 ) {
12291 $this->_logger->entrance();
12292
12293 $install = $this->get_install_by_blog_id( $context_blog_id );
12294
12295 if ( ! is_object( $install ) ) {
12296 // Site not connected.
12297 return;
12298 }
12299
12300 $this->update_multisite_data_after_site_deactivation( $context_blog_id );
12301
12302 $current_blog_id = get_current_blog_id();
12303
12304 $this->switch_to_blog( $context_blog_id );
12305
12306 // Send deactivation event.
12307 $this->sync_install( array(
12308 'is_active' => false,
12309 ) );
12310
12311 $this->switch_to_blog( $current_blog_id );
12312 }
12313
12314 /**
12315 * Executed after site deletion.
12316 *
12317 * @author Vova Feldman (@svovaf)
12318 * @since 2.0.0
12319 *
12320 * @param int $context_blog_id
12321 * @param bool $drop True if site's database tables should be dropped. Default is false.
12322 */
12323 public function _after_site_deleted_callback( $context_blog_id = 0, $drop = false ) {
12324 $this->_logger->entrance();
12325
12326 $install = $this->get_install_by_blog_id( $context_blog_id );
12327
12328 if ( ! is_object( $install ) ) {
12329 // Site not connected.
12330 return;
12331 }
12332
12333 $this->update_multisite_data_after_site_deactivation( $context_blog_id );
12334
12335 $current_blog_id = get_current_blog_id();
12336
12337 $this->switch_to_blog( $context_blog_id );
12338
12339 if ( $drop ) {
12340 // Delete install if dropping site DB.
12341 $this->delete_account_event();
12342 } else {
12343 // Send deactivation event.
12344 $this->sync_install( array(
12345 'is_active' => false,
12346 ) );
12347 }
12348
12349 $this->switch_to_blog( $current_blog_id );
12350 }
12351
12352 /**
12353 * Executed after site re-activation.
12354 *
12355 * @author Vova Feldman (@svovaf)
12356 * @since 2.0.0
12357 *
12358 * @param int $context_blog_id
12359 */
12360 public function _after_site_reactivated_callback( $context_blog_id = 0 ) {
12361 $this->_logger->entrance();
12362
12363 $install = $this->get_install_by_blog_id( $context_blog_id );
12364
12365 if ( ! is_object( $install ) ) {
12366 // Site not connected.
12367 return;
12368 }
12369
12370 if ( ! self::is_site_active( $context_blog_id ) ) {
12371 // Site not yet active (can be in spam mode, archived, deleted...).
12372 return;
12373 }
12374
12375 $current_blog_id = get_current_blog_id();
12376
12377 $this->switch_to_blog( $context_blog_id );
12378
12379 // Send re-activation event.
12380 $this->sync_install( array(
12381 'is_active' => true,
12382 ) );
12383
12384 $this->switch_to_blog( $current_blog_id );
12385 }
12386
12387 #endregion Multisite
12388
12389 /**
12390 * @author Leo Fajardo (@leorw)
12391 *
12392 * @param string $path
12393 * @param string $scheme
12394 * @param bool $network
12395 *
12396 * @return string
12397 */
12398 private function admin_url( $path = '', $scheme = 'admin', $network = true ) {
12399 return ( $this->_is_network_active && $network ) ?
12400 network_admin_url( $path, $scheme ) :
12401 admin_url( $path, $scheme );
12402 }
12403
12404 /**
12405 * Check if currently in a specified admin page.
12406 *
12407 * @author Vova Feldman (@svovaf)
12408 * @since 1.2.2.7
12409 *
12410 * @param string $page
12411 *
12412 * @return bool
12413 */
12414 function is_admin_page( $page ) {
12415 return ( $this->_menu->get_slug( $page ) === fs_request_get( 'page', '', 'get' ) );
12416 }
12417
12418 /**
12419 * Get module's main admin setting page URL.
12420 *
12421 * @author Vova Feldman (@svovaf)
12422 * @since 1.2.2.7
12423 *
12424 * @return string
12425 */
12426 function main_menu_url() {
12427 return $this->_menu->main_menu_url();
12428 }
12429
12430 /**
12431 * Check if currently on the theme's setting page or
12432 * on any of the Freemius added pages (via tabs).
12433 *
12434 * @author Vova Feldman (@svovaf)
12435 * @since 1.2.2.7
12436 *
12437 * @return bool
12438 */
12439 function is_theme_settings_page() {
12440 return fs_starts_with(
12441 fs_request_get( 'page', '', 'get' ),
12442 $this->_menu->get_slug()
12443 );
12444 }
12445
12446 /**
12447 * Plugin's account page + sync license URL.
12448 *
12449 * @author Vova Feldman (@svovaf)
12450 * @since 1.1.9.1
12451 *
12452 * @param bool|number $plugin_id
12453 * @param bool $add_action_nonce
12454 * @param array $params
12455 *
12456 * @return string
12457 */
12458 function _get_sync_license_url( $plugin_id = false, $add_action_nonce = true, $params = array() ) {
12459 if ( is_numeric( $plugin_id ) ) {
12460 $params['plugin_id'] = $plugin_id;
12461 }
12462
12463 return $this->get_account_url(
12464 $this->get_unique_affix() . '_sync_license',
12465 $params,
12466 $add_action_nonce
12467 );
12468 }
12469
12470 /**
12471 * Plugin's account URL.
12472 *
12473 * @author Vova Feldman (@svovaf)
12474 * @since 1.0.4
12475 *
12476 * @param bool|string $action
12477 * @param array $params
12478 *
12479 * @param bool $add_action_nonce
12480 *
12481 * @return string
12482 */
12483 function get_account_url( $action = false, $params = array(), $add_action_nonce = true ) {
12484 if ( is_string( $action ) ) {
12485 $params['fs_action'] = $action;
12486 }
12487
12488 self::require_pluggable_essentials();
12489
12490 return ( $add_action_nonce && is_string( $action ) ) ?
12491 fs_nonce_url( $this->_get_admin_page_url( 'account', $params ), $action ) :
12492 $this->_get_admin_page_url( 'account', $params );
12493 }
12494
12495 /**
12496 * @author Vova Feldman (@svovaf)
12497 * @since 1.2.0
12498 *
12499 * @param string $tab
12500 * @param bool $action
12501 * @param array $params
12502 * @param bool $add_action_nonce
12503 *
12504 * @return string
12505 *
12506 * @uses get_account_url()
12507 */
12508 function get_account_tab_url( $tab, $action = false, $params = array(), $add_action_nonce = true ) {
12509 $params['tab'] = $tab;
12510
12511 return $this->get_account_url( $action, $params, $add_action_nonce );
12512 }
12513
12514 /**
12515 * Plugin's account URL.
12516 *
12517 * @author Vova Feldman (@svovaf)
12518 * @since 1.0.4
12519 *
12520 * @param bool|string $topic
12521 * @param bool|string $message
12522 *
12523 * @return string
12524 */
12525 function contact_url( $topic = false, $message = false ) {
12526 $params = array();
12527 if ( is_string( $topic ) ) {
12528 $params['topic'] = $topic;
12529 }
12530 if ( is_string( $message ) ) {
12531 $params['message'] = $message;
12532 }
12533
12534 if ( $this->is_addon() ) {
12535 $params['addon_id'] = $this->get_id();
12536
12537 return $this->get_parent_instance()->_get_admin_page_url( 'contact', $params );
12538 } else {
12539 return $this->_get_admin_page_url( 'contact', $params );
12540 }
12541 }
12542
12543 /**
12544 * Add-on direct info URL.
12545 *
12546 * @author Vova Feldman (@svovaf)
12547 * @since 1.1.0
12548 *
12549 * @param string $slug
12550 *
12551 * @return string
12552 */
12553 function addon_url( $slug ) {
12554 return $this->_get_admin_page_url( 'addons', array(
12555 'slug' => $slug
12556 ) );
12557 }
12558
12559 /* Logger
12560 ------------------------------------------------------------------------------------------------------------------*/
12561 /**
12562 * @param string $id
12563 * @param bool $prefix_slug
12564 *
12565 * @return FS_Logger
12566 */
12567 function get_logger( $id = '', $prefix_slug = true ) {
12568 return FS_Logger::get_logger( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id );
12569 }
12570
12571 /**
12572 * Note: This method is used externally so don't delete it.
12573 *
12574 * @param $id
12575 * @param bool $load_options
12576 * @param bool $prefix_slug
12577 *
12578 * @return FS_Option_Manager
12579 */
12580 function get_options_manager( $id, $load_options = false, $prefix_slug = true ) {
12581 return FS_Option_Manager::get_manager( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id, $load_options );
12582 }
12583
12584 /* Security
12585 ------------------------------------------------------------------------------------------------------------------*/
12586 private static function _encrypt( $str ) {
12587 if ( is_null( $str ) ) {
12588 return null;
12589 }
12590
12591 /**
12592 * The encrypt/decrypt functions are used to protect
12593 * the user from messing up with some of the sensitive
12594 * data stored for the module as a JSON in the database.
12595 *
12596 * I used the same suggested hack by the theme review team.
12597 * For more details, look at the function `Base64UrlDecode()`
12598 * in `./sdk/FreemiusBase.php`.
12599 *
12600 * @todo Remove this hack once the base64 error is removed from the Theme Check.
12601 *
12602 * @author Vova Feldman (@svovaf)
12603 * @since 1.2.2
12604 */
12605 $fn = 'base64' . '_encode';
12606
12607 return $fn( $str );
12608 }
12609
12610 static function _decrypt( $str ) {
12611 if ( is_null( $str ) ) {
12612 return null;
12613 }
12614
12615 /**
12616 * The encrypt/decrypt functions are used to protect
12617 * the user from messing up with some of the sensitive
12618 * data stored for the module as a JSON in the database.
12619 *
12620 * I used the same suggested hack by the theme review team.
12621 * For more details, look at the function `Base64UrlDecode()`
12622 * in `./sdk/FreemiusBase.php`.
12623 *
12624 * @todo Remove this hack once the base64 error is removed from the Theme Check.
12625 *
12626 * @author Vova Feldman (@svovaf)
12627 * @since 1.2.2
12628 */
12629 $fn = 'base64' . '_decode';
12630
12631 return $fn( $str );
12632 }
12633
12634 /**
12635 * @author Vova Feldman (@svovaf)
12636 * @since 1.0.5
12637 *
12638 * @param FS_Entity $entity
12639 *
12640 * @return FS_Entity Return an encrypted clone entity.
12641 */
12642 private static function _encrypt_entity( FS_Entity $entity ) {
12643 $clone = clone $entity;
12644 $props = get_object_vars( $entity );
12645
12646 foreach ( $props as $key => $val ) {
12647 $clone->{$key} = self::_encrypt( $val );
12648 }
12649
12650 return $clone;
12651 }
12652
12653 /**
12654 * @author Vova Feldman (@svovaf)
12655 * @since 1.0.5
12656 *
12657 * @param FS_Entity $entity
12658 *
12659 * @return FS_Entity Return an decrypted clone entity.
12660 */
12661 private static function decrypt_entity( FS_Entity $entity ) {
12662 $clone = clone $entity;
12663 $props = get_object_vars( $entity );
12664
12665 foreach ( $props as $key => $val ) {
12666 $clone->{$key} = self::_decrypt( $val );
12667 }
12668
12669 return $clone;
12670 }
12671
12672 /**
12673 * Tries to activate account based on POST params.
12674 *
12675 * @author Vova Feldman (@svovaf)
12676 * @since 1.0.2
12677 *
12678 * @deprecated Not in use, outdated.
12679 */
12680 function _activate_account() {
12681 if ( $this->is_registered() ) {
12682 // Already activated.
12683 return;
12684 }
12685
12686 self::_clean_admin_content_section();
12687
12688 if ( fs_request_is_action( 'activate' ) && fs_request_is_post() ) {
12689 // check_admin_referer( 'activate_' . $this->_plugin->public_key );
12690
12691 // Verify matching plugin details.
12692 if ( $this->_plugin->id != fs_request_get( 'plugin_id' ) || $this->_slug != fs_request_get( 'plugin_slug' ) ) {
12693 return;
12694 }
12695
12696 $user = new FS_User();
12697 $user->id = fs_request_get( 'user_id' );
12698 $user->public_key = fs_request_get( 'user_public_key' );
12699 $user->secret_key = fs_request_get( 'user_secret_key' );
12700 $user->email = fs_request_get( 'user_email' );
12701 $user->first = fs_request_get( 'user_first' );
12702 $user->last = fs_request_get( 'user_last' );
12703 $user->is_verified = fs_request_get_bool( 'user_is_verified' );
12704
12705 $site = new FS_Site();
12706 $site->id = fs_request_get( 'install_id' );
12707 $site->public_key = fs_request_get( 'install_public_key' );
12708 $site->secret_key = fs_request_get( 'install_secret_key' );
12709 $site->plan_id = fs_request_get( 'plan_id' );
12710
12711 $plans = array();
12712 $plans_data = json_decode( urldecode( fs_request_get( 'plans' ) ) );
12713 foreach ( $plans_data as $p ) {
12714 $plan = new FS_Plugin_Plan( $p );
12715 if ( $site->plan_id == $plan->id ) {
12716 $plan->title = fs_request_get( 'plan_title' );
12717 $plan->name = fs_request_get( 'plan_name' );
12718 }
12719
12720 $plans[] = $plan;
12721 }
12722
12723 $this->_set_account( $user, $site, $plans );
12724
12725 // Reload the page with the keys.
12726 fs_redirect( $this->_get_admin_page_url() );
12727 }
12728 }
12729
12730 /**
12731 * @author Vova Feldman (@svovaf)
12732 * @since 1.0.7
12733 *
12734 * @param string $email
12735 *
12736 * @return FS_User|false
12737 */
12738 static function _get_user_by_email( $email ) {
12739 self::$_static_logger->entrance();
12740
12741 $email = trim( strtolower( $email ) );
12742
12743 $users = self::get_all_users();
12744
12745 if ( is_array( $users ) ) {
12746 foreach ( $users as $user ) {
12747 if ( $email === trim( strtolower( $user->email ) ) ) {
12748 return $user;
12749 }
12750 }
12751 }
12752
12753 return false;
12754 }
12755
12756 #----------------------------------------------------------------------------------
12757 #region Account (Loading, Updates & Activation)
12758 #----------------------------------------------------------------------------------
12759
12760 /***
12761 * Load account information (user + site).
12762 *
12763 * @author Vova Feldman (@svovaf)
12764 * @since 1.0.1
12765 */
12766 private function _load_account() {
12767 $this->_logger->entrance();
12768
12769 $this->do_action( 'before_account_load' );
12770
12771 $users = self::get_all_users();
12772 $plans = self::get_all_plans( $this->_module_type );
12773
12774 if ( $this->_logger->is_on() && is_admin() ) {
12775 $this->_logger->log( 'users = ' . var_export( $users, true ) );
12776 $this->_logger->log( 'plans = ' . var_export( $plans, true ) );
12777 }
12778
12779 $site = fs_is_network_admin() ?
12780 $this->get_network_install() :
12781 $this->get_install_by_blog_id();
12782
12783 if ( fs_is_network_admin() &&
12784 ! is_object( $site ) &&
12785 FS_Site::is_valid_id( $this->_storage->network_install_blog_id )
12786 ) {
12787 $first_install = $this->find_first_install();
12788
12789 if ( is_null( $first_install ) ) {
12790 unset( $this->_storage->network_install_blog_id );
12791 } else {
12792 $site = $first_install['install'];
12793 $this->_storage->network_install_blog_id = $first_install['blog_id'];
12794 }
12795 }
12796
12797 if ( is_object( $site ) &&
12798 is_numeric( $site->id ) &&
12799 is_numeric( $site->user_id ) &&
12800 FS_Plugin_Plan::is_valid_id( $site->plan_id )
12801 ) {
12802 // Load site.
12803 $this->_site = $site;
12804
12805 // Load plans.
12806 $this->_plans = $plans[ $this->_slug ];
12807 if ( ! is_array( $this->_plans ) || empty( $this->_plans ) ) {
12808 $this->_sync_plans();
12809 } else {
12810 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
12811 if ( $this->_plans[ $i ] instanceof FS_Plugin_Plan ) {
12812 $this->_plans[ $i ] = self::decrypt_entity( $this->_plans[ $i ] );
12813 } else {
12814 unset( $this->_plans[ $i ] );
12815 }
12816 }
12817 }
12818 }
12819
12820 $user = null;
12821 if ( fs_is_network_admin() && $this->_is_network_active ) {
12822 $user = $this->get_network_user();
12823 }
12824
12825 if ( is_object( $user ) ) {
12826 $this->_user = clone $user;
12827 } else if ( $this->_site ) {
12828 $user = self::_get_user_by_id( $this->_site->user_id );
12829
12830 if ( ! is_object( $user ) && FS_User::is_valid_id( $this->_storage->prev_user_id ) ) {
12831 /**
12832 * Try to load the previous owner. This recovery is used for the following use-case:
12833 * 1. Opt-in
12834 * 2. Cloning site1 to site2
12835 * 3. Ownership switch in site1 (same applies for site2)
12836 * 4. Install data sync on site2
12837 * 5. Now site2's install is associated with the new owner which does not exists locally.
12838 */
12839 $user = self::_get_user_by_id( $this->_storage->prev_user_id );
12840 }
12841
12842 if ( ! is_object( $user ) ) {
12843 /**
12844 * This is a special fault tolerance mechanism to handle a scenario that the user data is missing.
12845 */
12846 $user = $this->fetch_user_by_install();
12847 }
12848
12849 $this->_user = ( $user instanceof FS_User ) ?
12850 clone $user :
12851 null;
12852 }
12853
12854 if ( is_object( $this->_user ) ) {
12855 // Load licenses.
12856 $this->_licenses = $this->get_user_licenses( $this->_user->id );
12857 }
12858
12859 if ( is_object( $this->_site ) ) {
12860 $this->_license = $this->_get_license_by_id( $this->_site->license_id );
12861
12862 if ( $this->_site->version != $this->get_plugin_version() ) {
12863 // If stored install version is different than current installed plugin version,
12864 // then update plugin version event.
12865 $this->update_plugin_version_event();
12866 }
12867 }
12868
12869 if ( $this->is_theme() ) {
12870 $this->_register_account_hooks();
12871 }
12872 }
12873
12874 /**
12875 * Special user recovery mechanism.
12876 *
12877 * @author Vova Feldman (@svovaf)
12878 * @since 2.0.0
12879 *
12880 * @return \FS_User|mixed
12881 */
12882 private function fetch_user_by_install() {
12883 $api = $this->get_api_site_scope();
12884
12885 $uid = $this->get_anonymous_id();
12886 $request_path = "/users/{$this->_site->user_id}.json?uid={$uid}";
12887
12888 $result = $api->get( $request_path, false, WP_FS__TIME_10_MIN_IN_SEC );
12889
12890 if ( $this->is_api_result_entity( $result ) ) {
12891 $user = new FS_User( $result );
12892 $this->_user = $user;
12893 $this->_store_user();
12894
12895 return $user;
12896 }
12897
12898 $error_code = FS_Api::get_error_code( $result );
12899
12900 if ( in_array( $error_code, array( 'invalid_unique_id', 'user_cannot_be_recovered' ) ) ) {
12901 /**
12902 * Those API errors will continue coming and are not recoverable with the
12903 * current site's data. Therefore, extend the API call's cached result to 7 days.
12904 */
12905 $api->update_cache_expiration( $request_path, WP_FS__TIME_WEEK_IN_SEC );
12906 }
12907
12908 return $result;
12909 }
12910
12911 /**
12912 * @author Vova Feldman (@svovaf)
12913 * @since 1.0.1
12914 *
12915 * @param FS_User $user
12916 * @param FS_Site $site
12917 * @param bool|array $plans
12918 */
12919 private function _set_account( FS_User $user, FS_Site $site, $plans = false ) {
12920 $site->user_id = $user->id;
12921
12922 $this->_site = $site;
12923 $this->_user = $user;
12924 if ( false !== $plans ) {
12925 $this->_plans = $plans;
12926 }
12927
12928 $this->send_install_update();
12929
12930 $this->_store_account();
12931
12932 }
12933
12934 /**
12935 * @author Vova Feldman (@svovaf)
12936 * @since 1.1.7.4
12937 *
12938 * @param array $override_with
12939 * @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.
12940 *
12941 * @return array
12942 */
12943 function get_opt_in_params( $override_with = array(), $network_level_or_blog_id = null ) {
12944 $this->_logger->entrance();
12945
12946 $current_user = self::_get_current_wp_user();
12947
12948 $activation_action = $this->get_unique_affix() . '_activate_new';
12949 $return_url = $this->is_anonymous() ?
12950 // If skipped already, then return to the account page.
12951 $this->get_account_url( $activation_action, array(), false ) :
12952 // Return to the module's main page.
12953 $this->get_after_activation_url( 'after_connect_url', array( 'fs_action' => $activation_action ) );
12954
12955 $params = array(
12956 'user_firstname' => $current_user->user_firstname,
12957 'user_lastname' => $current_user->user_lastname,
12958 'user_nickname' => $current_user->user_nicename,
12959 'user_email' => $current_user->user_email,
12960 'user_ip' => WP_FS__REMOTE_ADDR,
12961 'plugin_slug' => $this->_slug,
12962 'plugin_id' => $this->get_id(),
12963 'plugin_public_key' => $this->get_public_key(),
12964 'plugin_version' => $this->get_plugin_version(),
12965 'return_url' => fs_nonce_url( $return_url, $activation_action ),
12966 'account_url' => fs_nonce_url( $this->_get_admin_page_url(
12967 'account',
12968 array( 'fs_action' => 'sync_user' )
12969 ), 'sync_user' ),
12970 'platform_version' => get_bloginfo( 'version' ),
12971 'sdk_version' => $this->version,
12972 'programming_language_version' => phpversion(),
12973 'is_premium' => $this->is_premium(),
12974 'is_active' => true,
12975 'is_uninstalled' => false,
12976 );
12977
12978 if ( true === $network_level_or_blog_id ) {
12979 if ( ! isset( $override_with['sites'] ) ) {
12980 $params['sites'] = array();
12981
12982 $sites = self::get_sites();
12983
12984 foreach ( $sites as $site ) {
12985 $blog_id = self::get_site_blog_id( $site );
12986 if ( ! $this->is_site_delegated_connection( $blog_id ) &&
12987 ! $this->is_installed_on_site( $blog_id )
12988 ) {
12989 $params['sites'][] = $this->get_site_info( $site );
12990 }
12991 }
12992 }
12993 } else {
12994 $site = is_numeric( $network_level_or_blog_id ) ?
12995 array( 'blog_id' => $network_level_or_blog_id ) :
12996 null;
12997
12998 $site = $this->get_site_info( $site );
12999
13000 $params = array_merge( $params, array(
13001 'site_uid' => $site['uid'],
13002 'site_url' => $site['url'],
13003 'site_name' => $site['title'],
13004 'language' => $site['language'],
13005 'charset' => $site['charset'],
13006 ) );
13007 }
13008
13009 if ( $this->is_pending_activation() &&
13010 ! empty( $this->_storage->pending_license_key )
13011 ) {
13012 $params['license_key'] = $this->_storage->pending_license_key;
13013 }
13014
13015 if ( WP_FS__SKIP_EMAIL_ACTIVATION && $this->has_secret_key() ) {
13016 // Even though rand() is known for its security issues,
13017 // the timestamp adds another layer of protection.
13018 // It would be very hard for an attacker to get the secret key form here.
13019 // Plus, this should never run in production since the secret should never
13020 // be included in the production version.
13021 $params['ts'] = WP_FS__SCRIPT_START_TIME;
13022 $params['salt'] = md5( uniqid( rand() ) );
13023 $params['secure'] = md5(
13024 $params['ts'] .
13025 $params['salt'] .
13026 $this->get_secret_key()
13027 );
13028 }
13029
13030 return array_merge( $params, $override_with );
13031 }
13032
13033 /**
13034 * 1. If successful opt-in or pending activation returns the next page that the user should be redirected to.
13035 * 2. If there was an API error, return the API result.
13036 *
13037 * @author Vova Feldman (@svovaf)
13038 * @since 1.1.7.4
13039 *
13040 * @param string|bool $email
13041 * @param string|bool $first
13042 * @param string|bool $last
13043 * @param string|bool $license_key
13044 * @param bool $is_uninstall If "true", this means that the module is currently being uninstalled.
13045 * In this case, the user and site info will be sent to the server but no
13046 * data will be saved to the WP installation's database.
13047 * @param number|bool $trial_plan_id
13048 * @param bool $is_disconnected Whether or not to opt in without tracking.
13049 * @param array $sites If network-level opt-in, an array of containing details of sites.
13050 *
13051 * @return string|object
13052 * @use WP_Error
13053 */
13054 function opt_in(
13055 $email = false,
13056 $first = false,
13057 $last = false,
13058 $license_key = false,
13059 $is_uninstall = false,
13060 $trial_plan_id = false,
13061 $is_disconnected = false,
13062 $sites = array()
13063 ) {
13064 $this->_logger->entrance();
13065
13066 if ( false === $email ) {
13067 $current_user = self::_get_current_wp_user();
13068 $email = $current_user->user_email;
13069 }
13070
13071 /**
13072 * @since 1.2.1 If activating with license key, ignore the context-user
13073 * since the user will be automatically loaded from the license.
13074 */
13075 if ( empty( $license_key ) ) {
13076 // Clean up pending license if opt-ing in again.
13077 $this->_storage->remove( 'pending_license_key' );
13078
13079 if ( ! $is_uninstall ) {
13080 $fs_user = Freemius::_get_user_by_email( $email );
13081 if ( is_object( $fs_user ) && ! $this->is_pending_activation() ) {
13082 return $this->install_with_current_user(
13083 false,
13084 $trial_plan_id,
13085 $sites
13086 );
13087 }
13088 }
13089 }
13090
13091 $user_info = array();
13092 if ( ! empty( $email ) ) {
13093 $user_info['user_email'] = $email;
13094 }
13095 if ( ! empty( $first ) ) {
13096 $user_info['user_firstname'] = $first;
13097 }
13098 if ( ! empty( $last ) ) {
13099 $user_info['user_lastname'] = $last;
13100 }
13101
13102 if ( ! empty( $sites ) ) {
13103 $is_network = true;
13104
13105 $user_info['sites'] = $sites;
13106 } else {
13107 $is_network = false;
13108 }
13109
13110 $params = $this->get_opt_in_params( $user_info, $is_network );
13111
13112 $filtered_license_key = false;
13113 if ( is_string( $license_key ) ) {
13114 $filtered_license_key = $this->apply_filters( 'license_key', $license_key );
13115 $params['license_key'] = $filtered_license_key;
13116 } else if ( FS_Plugin_Plan::is_valid_id( $trial_plan_id ) ) {
13117 $params['trial_plan_id'] = $trial_plan_id;
13118 }
13119
13120 if ( $is_uninstall ) {
13121 $params['uninstall_params'] = array(
13122 'reason_id' => $this->_storage->uninstall_reason->id,
13123 'reason_info' => $this->_storage->uninstall_reason->info
13124 );
13125 }
13126
13127 if ( isset( $params['license_key'] ) ) {
13128 $fs_user = Freemius::_get_user_by_email( $email );
13129
13130 if ( is_object( $fs_user ) ) {
13131 /**
13132 * If opting in with a context license and the context WP Admin user already opted in
13133 * before from the current site, add the user context security params to avoid the
13134 * unnecessary email activation when the context license is owned by the same context user.
13135 *
13136 * @author Leo Fajardo (@leorw)
13137 * @since 1.2.3
13138 */
13139 $params = array_merge( $params, FS_Security::instance()->get_context_params(
13140 $fs_user,
13141 false,
13142 'install_with_existing_user'
13143 ) );
13144 }
13145 }
13146
13147 $params['is_disconnected'] = $is_disconnected;
13148 $params['format'] = 'json';
13149
13150 $request = array(
13151 'method' => 'POST',
13152 'body' => $params,
13153 'timeout' => WP_FS__DEBUG_SDK ? 60 : 30,
13154 );
13155
13156 $url = WP_FS__ADDRESS . '/action/service/user/install/';
13157
13158 if ( WP_FS__DEBUG_SDK || isset( $_COOKIE['XDEBUG_SESSION'] ) ) {
13159 $url = add_query_arg( 'XDEBUG_SESSION_START', rand( 0, 9999999 ), $url );
13160 $url = add_query_arg( 'XDEBUG_SESSION', 'PHPSTORM', $url );
13161
13162 $request['cookies'] = array(
13163 new WP_Http_Cookie( array(
13164 'name' => 'XDEBUG_SESSION',
13165 'value' => 'PHPSTORM',
13166 ) )
13167 );
13168 }
13169
13170 $response = wp_remote_post( $url, $request );
13171
13172 if ( $response instanceof WP_Error ) {
13173 if ( 'https://' === substr( $url, 0, 8 ) &&
13174 isset( $response->errors ) &&
13175 isset( $response->errors['http_request_failed'] )
13176 ) {
13177 $http_error = strtolower( $response->errors['http_request_failed'][0] );
13178
13179 if ( false !== strpos( $http_error, 'ssl' ) ||
13180 false !== strpos( $http_error, 'curl error 35' )
13181 ) {
13182 // Failed due to old version of cURL or Open SSL (SSLv3 is not supported by CloudFlare).
13183 $url = 'http://' . substr( $url, 8 );
13184
13185 $response = wp_remote_post( $url, array(
13186 'method' => 'POST',
13187 'body' => $params,
13188 'timeout' => 15,
13189 ) );
13190 }
13191 }
13192 }
13193
13194 if ( is_wp_error( $response ) ) {
13195 /**
13196 * @var WP_Error $response
13197 */
13198 $result = new stdClass();
13199
13200 $error_code = $response->get_error_code();
13201 $error_type = str_replace( ' ', '', ucwords( str_replace( '_', ' ', $error_code ) ) );
13202
13203 $result->error = (object) array(
13204 'type' => $error_type,
13205 'message' => $response->get_error_message(),
13206 'code' => $error_code,
13207 'http' => 402
13208 );
13209
13210 return $result;
13211 }
13212
13213 // Module is being uninstalled, don't handle the returned data.
13214 if ( $is_uninstall ) {
13215 return true;
13216 }
13217
13218 /**
13219 * 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.
13220 *
13221 * @author Vova Feldman (@svovaf)
13222 * @since 1.2.3
13223 * @link https://themes.trac.wordpress.org/ticket/46134#comment:5
13224 * @link https://themes.trac.wordpress.org/ticket/46134#comment:9
13225 * @link https://themes.trac.wordpress.org/ticket/46134#comment:12
13226 * @link https://themes.trac.wordpress.org/ticket/46134#comment:14
13227 */
13228 $decoded = is_string( $response['body'] ) ?
13229 json_decode( $response['body'] ) :
13230 null;
13231
13232 if ( empty( $decoded ) ) {
13233 return false;
13234 }
13235
13236 if ( ! $this->is_api_result_object( $decoded ) ) {
13237 if ( ! empty( $params['license_key'] ) ) {
13238 // Pass the fully entered license key to the failure handler.
13239 $params['license_key'] = $license_key;
13240 }
13241
13242 return $is_uninstall ?
13243 $decoded :
13244 $this->apply_filters( 'after_install_failure', $decoded, $params );
13245 } else if ( isset( $decoded->pending_activation ) && $decoded->pending_activation ) {
13246 if ( $is_network ) {
13247 $site_ids = array();
13248 foreach ( $sites as $site ) {
13249 $site_ids[] = $site['blog_id'];
13250 }
13251
13252 /**
13253 * Store the sites so that they can be installed once the user has clicked on the activation link
13254 * in the email.
13255 *
13256 * @author Leo Fajardo (@leorw)
13257 */
13258 $this->_storage->pending_sites_info = array(
13259 'blog_ids' => $site_ids,
13260 'license_key' => $license_key,
13261 'trial_plan_id' => $trial_plan_id
13262 );
13263 }
13264
13265 // Pending activation, add message.
13266 return $this->set_pending_confirmation(
13267 ( isset( $decoded->email ) ?
13268 $decoded->email :
13269 true ),
13270 false,
13271 $filtered_license_key,
13272 ! empty( $params['trial_plan_id'] )
13273 );
13274 } else if ( isset( $decoded->install_secret_key ) ) {
13275 return $this->install_with_new_user(
13276 $decoded->user_id,
13277 $decoded->user_public_key,
13278 $decoded->user_secret_key,
13279 $decoded->install_id,
13280 $decoded->install_public_key,
13281 $decoded->install_secret_key,
13282 false
13283 );
13284 } else if ( is_array( $decoded->installs ) ) {
13285 return $this->install_many_with_new_user(
13286 $decoded->user_id,
13287 $decoded->user_public_key,
13288 $decoded->user_secret_key,
13289 $decoded->installs,
13290 false
13291 );
13292 }
13293
13294 return $decoded;
13295 }
13296
13297 /**
13298 * Set user and site identities.
13299 *
13300 * @author Vova Feldman (@svovaf)
13301 * @since 1.0.9
13302 *
13303 * @param FS_User $user
13304 * @param FS_Site $site
13305 * @param bool $redirect
13306 * @param bool $auto_install Since 1.2.1.7 If `true` and setting up an account with a valid license, will
13307 * redirect (or return a URL) to the account page with a special parameter to
13308 * trigger the auto installation processes.
13309 *
13310 * @return string If redirect is `false`, returns the next page the user should be redirected to.
13311 */
13312 function setup_account(
13313 FS_User $user,
13314 FS_Site $site,
13315 $redirect = true,
13316 $auto_install = false
13317 ) {
13318 return $this->setup_network_account(
13319 $user,
13320 array( $site ),
13321 $redirect,
13322 $auto_install,
13323 false
13324 );
13325 }
13326
13327 /**
13328 * Set user and site identities.
13329 *
13330 * @author Vova Feldman (@svovaf)
13331 * @since 2.0.0
13332 *
13333 * @param FS_User $user
13334 * @param FS_Site[] $installs
13335 * @param bool $redirect
13336 * @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.
13337 * @param bool $is_network_level_opt_in
13338 *
13339 * @return string If redirect is `false`, returns the next page the user should be redirected to.
13340 */
13341 function setup_network_account(
13342 FS_User $user,
13343 array $installs,
13344 $redirect = true,
13345 $auto_install = false,
13346 $is_network_level_opt_in = true
13347 ) {
13348 $first_install = $installs[0];
13349
13350 $this->_user = $user;
13351 $this->_site = $first_install;
13352
13353 $this->_sync_plans();
13354
13355 if ( 1 < count( $installs ) ) {
13356 // Only network level opt-in can have more than one install.
13357 $is_network_level_opt_in = true;
13358 }
13359 // $is_network_level_opt_in = self::is_ajax_action_static( 'network_activate', $this->_module_id );
13360 // If Freemius was OFF before, turn it on.
13361 $this->turn_on();
13362
13363 if ( ! $this->_is_network_active || ! $is_network_level_opt_in ) {
13364 $this->_set_account( $user, $first_install );
13365
13366 $this->do_action( 'after_account_connection', $user, $first_install );
13367 } else {
13368 // Map site addresses to their blog IDs.
13369 $address_to_blog_map = $this->get_address_to_blog_map();
13370
13371 $first_blog_id = null;
13372 $blog_2_install_map = array();
13373 foreach ( $installs as $install ) {
13374 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
13375 $blog_id = $address_to_blog_map[ $address ];
13376
13377 $this->_store_site( true, $blog_id, $install );
13378
13379 if ( is_null( $first_blog_id ) ) {
13380 $first_blog_id = $blog_id;
13381 }
13382
13383 $blog_2_install_map[ $blog_id ] = $install;
13384 }
13385
13386 if ( ! FS_User::is_valid_id( $this->_storage->network_user_id ) ||
13387 ! is_object( self::_get_user_by_id( $this->_storage->network_user_id ) )
13388 ) {
13389 // Store network user.
13390 $this->_storage->network_user_id = $this->_user->id;
13391 }
13392
13393 if ( ! FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) ) {
13394 $this->_storage->network_install_blog_id = $first_blog_id;
13395 }
13396
13397 if ( count( $installs ) === count( $address_to_blog_map ) ) {
13398 // Super-admin opted-in for all sites in the network.
13399 $this->_storage->is_network_connected = true;
13400 }
13401
13402 $this->_store_licenses( false );
13403
13404 self::$_accounts->store();
13405
13406 // Don't sync the installs data on network upgrade
13407 if ( ! $this->network_upgrade_mode_completed() ) {
13408 $this->send_installs_update();
13409 }
13410
13411 // Switch install context back to the first install.
13412 $this->_site = $first_install;
13413
13414 $current_blog = get_current_blog_id();
13415
13416 foreach ( $blog_2_install_map as $blog_id => $install ) {
13417 $this->switch_to_blog( $blog_id );
13418
13419 $this->do_action( 'after_account_connection', $user, $install );
13420 }
13421
13422 $this->switch_to_blog( $current_blog );
13423
13424 $this->do_action( 'after_network_account_connection', $user, $blog_2_install_map );
13425 }
13426
13427 if ( is_numeric( $first_install->license_id ) ) {
13428 $this->_license = $this->_get_license_by_id( $first_install->license_id );
13429 }
13430
13431 $this->_admin_notices->remove_sticky( 'connect_account' );
13432
13433 if ( $this->is_pending_activation() || ! $this->has_settings_menu() ) {
13434 // Remove pending activation sticky notice (if still exist).
13435 $this->_admin_notices->remove_sticky( 'activation_pending' );
13436
13437 // Remove plugin from pending activation mode.
13438 unset( $this->_storage->is_pending_activation );
13439
13440 if ( ! $this->is_paying_or_trial() ) {
13441 $this->_admin_notices->add_sticky(
13442 sprintf( $this->get_text_inline( '%s activation was successfully completed.', 'plugin-x-activation-message' ), '<b>' . $this->get_plugin_name() . '</b>' ),
13443 'activation_complete'
13444 );
13445 }
13446 }
13447
13448 if ( $this->is_paying_or_trial() ) {
13449 if ( ! $this->is_premium() ||
13450 ! $this->has_premium_version() ||
13451 ! $this->has_settings_menu()
13452 ) {
13453 if ( $this->is_paying() ) {
13454 $this->_admin_notices->add_sticky(
13455 sprintf(
13456 $this->get_text_inline( 'Your account was successfully activated with the %s plan.', 'activation-with-plan-x-message' ),
13457 $this->get_plan_title()
13458 ) . $this->get_complete_upgrade_instructions(),
13459 'plan_upgraded',
13460 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
13461 );
13462 } else {
13463 $trial_plan = $this->get_trial_plan();
13464
13465 $this->_admin_notices->add_sticky(
13466 sprintf(
13467 $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ),
13468 '<i>' . $this->get_plugin_name() . '</i>'
13469 ) . $this->get_complete_upgrade_instructions( $trial_plan->title ),
13470 'trial_started',
13471 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
13472 );
13473 }
13474 }
13475
13476 $this->_admin_notices->remove_sticky( array(
13477 'trial_promotion',
13478 ) );
13479 }
13480
13481 $plugin_id = fs_request_get( 'plugin_id', false );
13482
13483 // Store activation time ONLY for plugins & themes (not add-ons).
13484 if ( ! is_numeric( $plugin_id ) || ( $plugin_id == $this->_plugin->id ) ) {
13485 if ( empty( $this->_storage->activation_timestamp ) ) {
13486 $this->_storage->activation_timestamp = WP_FS__SCRIPT_START_TIME;
13487 }
13488 }
13489
13490 $next_page = '';
13491
13492 $extra = array();
13493 if ( $auto_install ) {
13494 $extra['auto_install'] = 'true';
13495 }
13496
13497 if ( is_numeric( $plugin_id ) ) {
13498 /**
13499 * @author Leo Fajardo (@leorw)
13500 * @since 1.2.1.6
13501 *
13502 * Also sync the license after an anonymous user subscribes.
13503 */
13504 if ( $this->is_anonymous() || $plugin_id != $this->_plugin->id ) {
13505 // Add-on was installed - sync license right after install.
13506 $next_page = $this->_get_sync_license_url( $plugin_id, true, $extra );
13507 }
13508 } else {
13509 /**
13510 * @author Vova Feldman (@svovaf)
13511 * @since 1.1.9 If site installed with a valid license, sync license.
13512 */
13513 if ( $this->is_paying() ) {
13514 $this->_sync_plugin_license(
13515 true,
13516 // Installs data is already synced in the beginning of this method directly or via _set_account().
13517 false
13518 );
13519 }
13520
13521 // Reload the page with the keys.
13522 $next_page = $this->is_anonymous() ?
13523 // If user previously skipped, redirect to account page.
13524 $this->get_account_url( false, $extra ) :
13525 $this->get_after_activation_url( 'after_connect_url', array(), $is_network_level_opt_in );
13526 }
13527
13528 if ( ! empty( $next_page ) && $redirect ) {
13529 fs_redirect( $next_page );
13530 }
13531
13532 return $next_page;
13533 }
13534
13535 /**
13536 * Install plugin with new user information after approval.
13537 *
13538 * @author Vova Feldman (@svovaf)
13539 * @since 1.0.7
13540 */
13541 function _install_with_new_user() {
13542 $this->_logger->entrance();
13543
13544 if ( $this->is_registered() ) {
13545 return;
13546 }
13547
13548 if ( ( $this->is_plugin() && fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) ||
13549 // @todo This logic should be improved because it's executed on every load of a theme.
13550 $this->is_theme()
13551 ) {
13552 // check_admin_referer( $this->_slug . '_activate_new' );
13553
13554 if ( fs_request_has( 'user_secret_key' ) ) {
13555 if ( fs_is_network_admin() && isset( $this->_storage->pending_sites_info ) ) {
13556 $pending_sites_info = $this->_storage->pending_sites_info;
13557
13558 $this->install_many_pending_with_user(
13559 fs_request_get( 'user_id' ),
13560 fs_request_get( 'user_public_key' ),
13561 fs_request_get( 'user_secret_key' ),
13562 $pending_sites_info['blog_ids'],
13563 $pending_sites_info['license_key'],
13564 $pending_sites_info['trial_plan_id']
13565 );
13566 } else {
13567 $this->install_with_new_user(
13568 fs_request_get( 'user_id' ),
13569 fs_request_get( 'user_public_key' ),
13570 fs_request_get( 'user_secret_key' ),
13571 fs_request_get( 'install_id' ),
13572 fs_request_get( 'install_public_key' ),
13573 fs_request_get( 'install_secret_key' ),
13574 true,
13575 fs_request_get_bool( 'auto_install' )
13576 );
13577 }
13578 } else if ( fs_request_has( 'pending_activation' ) ) {
13579 $this->set_pending_confirmation( fs_request_get( 'user_email' ), true );
13580 }
13581 }
13582 }
13583
13584 /**
13585 * @author Vova Feldman (@svovaf)
13586 * @since 2.0.0
13587 *
13588 * @param number $id
13589 * @param string $public_key
13590 * @param string $secret_key
13591 *
13592 * @return \FS_User
13593 */
13594 private function setup_user( $id, $public_key, $secret_key ) {
13595 $user = self::_get_user_by_id( $id );
13596
13597 if ( is_object( $user ) ) {
13598 $this->_user = $user;
13599 } else {
13600 $user = new FS_User();
13601 $user->id = $id;
13602 $user->public_key = $public_key;
13603 $user->secret_key = $secret_key;
13604
13605 $this->_user = $user;
13606 $user_result = $this->get_api_user_scope()->get();
13607 $user = new FS_User( $user_result );
13608
13609 $this->_user = $user;
13610 $this->_store_user();
13611 }
13612
13613 return $user;
13614 }
13615
13616 /**
13617 * Install plugin with new user.
13618 *
13619 * @author Vova Feldman (@svovaf)
13620 * @since 1.1.7.4
13621 *
13622 * @param number $user_id
13623 * @param string $user_public_key
13624 * @param string $user_secret_key
13625 * @param number $install_id
13626 * @param string $install_public_key
13627 * @param string $install_secret_key
13628 * @param bool $redirect
13629 * @param bool $auto_install Since 1.2.1.7 If `true` and setting up an account with a valid license, will
13630 * redirect (or return a URL) to the account page with a special parameter to
13631 * trigger the auto installation processes.
13632 *
13633 * @return string If redirect is `false`, returns the next page the user should be redirected to.
13634 */
13635 private function install_with_new_user(
13636 $user_id,
13637 $user_public_key,
13638 $user_secret_key,
13639 $install_id,
13640 $install_public_key,
13641 $install_secret_key,
13642 $redirect = true,
13643 $auto_install = false
13644 ) {
13645 /**
13646 * This method is also executed after opting in with a license key since the
13647 * license can be potentially associated with a different owner.
13648 *
13649 * @since 2.0.0
13650 */
13651 $user = self::_get_user_by_id( $user_id );
13652
13653 if ( ! is_object( $user ) ) {
13654 $user = new FS_User();
13655 $user->id = $user_id;
13656 $user->public_key = $user_public_key;
13657 $user->secret_key = $user_secret_key;
13658
13659 $this->_user = $user;
13660 $user_result = $this->get_api_user_scope()->get();
13661 $user = new FS_User( $user_result );
13662 }
13663
13664 $this->_user = $user;
13665
13666 $site = new FS_Site();
13667 $site->id = $install_id;
13668 $site->public_key = $install_public_key;
13669 $site->secret_key = $install_secret_key;
13670
13671 $this->_site = $site;
13672 $site_result = $this->get_api_site_scope()->get();
13673 $site = new FS_Site( $site_result );
13674 $this->_site = $site;
13675
13676 return $this->setup_account(
13677 $this->_user,
13678 $this->_site,
13679 $redirect,
13680 $auto_install
13681 );
13682 }
13683
13684 /**
13685 * Install plugin with user.
13686 *
13687 * @author Leo Fajardo (@leorw)
13688 * @since 2.0.0
13689 *
13690 * @param number $user_id
13691 * @param string $user_public_key
13692 * @param string $user_secret_key
13693 * @param array $site_ids
13694 * @param bool $license_key
13695 * @param bool $trial_plan_id
13696 * @param bool $redirect
13697 *
13698 * @return string If redirect is `false`, returns the next page the user should be redirected to.
13699 */
13700 private function install_many_pending_with_user(
13701 $user_id,
13702 $user_public_key,
13703 $user_secret_key,
13704 $site_ids,
13705 $license_key = false,
13706 $trial_plan_id = false,
13707 $redirect = true
13708 ) {
13709 $user = $this->setup_user( $user_id, $user_public_key, $user_secret_key );
13710
13711 $sites = array();
13712 foreach ( $site_ids as $site_id ) {
13713 $sites[] = $this->get_site_info( array( 'blog_id' => $site_id ) );
13714 }
13715
13716 $this->install_with_user( $user, $license_key, $trial_plan_id, $redirect, true, $sites );
13717 }
13718
13719 /**
13720 * Multi-site install with a new user.
13721 *
13722 * @author Vova Feldman (@svovaf)
13723 * @since 2.0.0
13724 *
13725 * @param number $user_id
13726 * @param string $user_public_key
13727 * @param string $user_secret_key
13728 * @param object[] $installs
13729 * @param bool $redirect
13730 * @param bool $auto_install Since 1.2.1.7 If `true` and setting up an account with a valid license, will
13731 * redirect (or return a URL) to the account page with a special parameter to
13732 * trigger the auto installation processes.
13733 *
13734 * @return string If redirect is `false`, returns the next page the user should be redirected to.
13735 */
13736 private function install_many_with_new_user(
13737 $user_id,
13738 $user_public_key,
13739 $user_secret_key,
13740 array $installs,
13741 $redirect = true,
13742 $auto_install = false
13743 ) {
13744 $this->setup_user( $user_id, $user_public_key, $user_secret_key );
13745
13746 $install_ids = array();
13747
13748 foreach ( $installs as $install ) {
13749 $install_ids[] = $install->id;
13750 }
13751
13752 $left = count( $install_ids );
13753 $offset = 0;
13754
13755 $installs = array();
13756 while ( $left > 0 ) {
13757 $result = $this->get_api_user_scope()->get( "/plugins/{$this->_module_id}/installs.json?ids=" . implode( ',', array_slice( $install_ids, $offset, 25 ) ) );
13758
13759 if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
13760 // @todo Handle API error.
13761 }
13762
13763 $installs = array_merge( $installs, $result->installs );
13764
13765 $left -= 25;
13766 }
13767
13768 foreach ( $installs as &$install ) {
13769 $install = new FS_Site( $install );
13770 }
13771
13772 return $this->setup_network_account(
13773 $this->_user,
13774 $installs,
13775 $redirect,
13776 $auto_install
13777 );
13778 }
13779
13780 /**
13781 * @author Vova Feldman (@svovaf)
13782 * @since 1.1.7.4
13783 *
13784 * @param string|bool $email
13785 * @param bool $redirect
13786 * @param string|bool $license_key Since 1.2.1.5
13787 * @param bool $is_pending_trial Since 1.2.1.5
13788 *
13789 * @return string Since 1.2.1.5 if $redirect is `false`, return the pending activation page.
13790 */
13791 private function set_pending_confirmation(
13792 $email = false,
13793 $redirect = true,
13794 $license_key = false,
13795 $is_pending_trial = false
13796 ) {
13797 if ( $this->_ignore_pending_mode ) {
13798 /**
13799 * If explicitly asked to ignore pending mode, set to anonymous mode
13800 * if require confirmation before finalizing the opt-in.
13801 *
13802 * @author Vova Feldman
13803 * @since 1.2.1.6
13804 */
13805 $this->skip_connection( null, fs_is_network_admin() );
13806 } else {
13807 // Install must be activated via email since
13808 // user with the same email already exist.
13809 $this->_storage->is_pending_activation = true;
13810 $this->_add_pending_activation_notice( $email, $is_pending_trial );
13811 }
13812
13813 if ( ! empty( $license_key ) ) {
13814 $this->_storage->pending_license_key = $license_key;
13815 }
13816
13817 // Remove the opt-in sticky notice.
13818 $this->_admin_notices->remove_sticky( array(
13819 'connect_account',
13820 'trial_promotion',
13821 ) );
13822
13823 $next_page = $this->get_after_activation_url( 'after_pending_connect_url' );
13824
13825 // Reload the page with with pending activation message.
13826 if ( $redirect ) {
13827 fs_redirect( $next_page );
13828 }
13829
13830 return $next_page;
13831 }
13832
13833 /**
13834 * Install plugin with current logged WP user info.
13835 *
13836 * @author Vova Feldman (@svovaf)
13837 * @since 1.0.7
13838 */
13839 function _install_with_current_user() {
13840 $this->_logger->entrance();
13841
13842 if ( $this->is_registered() ) {
13843 return;
13844 }
13845
13846 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) && fs_request_is_post() ) {
13847 // check_admin_referer( 'activate_existing_' . $this->_plugin->public_key );
13848
13849 /**
13850 * @author Vova Feldman (@svovaf)
13851 * @since 1.1.9 Add license key if given.
13852 */
13853 $license_key = fs_request_get( 'license_secret_key' );
13854
13855 $this->install_with_current_user( $license_key );
13856 }
13857 }
13858
13859
13860 /**
13861 * @author Vova Feldman (@svovaf)
13862 * @since 1.1.7.4
13863 *
13864 * @param string|bool $license_key
13865 * @param number|bool $trial_plan_id
13866 * @param array $sites Since 2.0.0
13867 * @param bool $redirect
13868 *
13869 * @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.
13870 */
13871 private function install_with_current_user(
13872 $license_key = false,
13873 $trial_plan_id = false,
13874 $sites = array(),
13875 $redirect = true
13876 ) {
13877 // Get current logged WP user.
13878 $current_user = self::_get_current_wp_user();
13879
13880 // Find the relevant FS user by the email.
13881 $user = self::_get_user_by_email( $current_user->user_email );
13882
13883 return $this->install_with_user( $user, $license_key, $trial_plan_id, $redirect, true, $sites );
13884 }
13885
13886 /**
13887 * @author Vova Feldman (@svovaf)
13888 * @since 2.0.0
13889 *
13890 * @param \FS_User $user
13891 * @param string|bool $license_key
13892 * @param number|bool $trial_plan_id
13893 * @param bool $redirect
13894 * @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.
13895 * @param array $sites Since 2.0.0. If not empty, should be a collection of site details for the bulk install API request.
13896 *
13897 * @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.
13898 */
13899 private function install_with_user(
13900 FS_User $user,
13901 $license_key = false,
13902 $trial_plan_id = false,
13903 $redirect = true,
13904 $setup_account = true,
13905 $sites = array()
13906 ) {
13907 // We have to set the user before getting user scope API handler.
13908 $this->_user = $user;
13909
13910 // Install the plugin.
13911 $result = $this->create_installs_with_user(
13912 $user,
13913 $license_key,
13914 $trial_plan_id,
13915 $sites,
13916 $redirect
13917 );
13918
13919 if ( ! $this->is_api_result_entity( $result ) &&
13920 ! $this->is_api_result_object( $result, 'installs' )
13921 ) {
13922 // @todo Handler potential API error of the $result
13923 }
13924
13925 if ( empty( $sites ) ) {
13926 $site = new FS_Site( $result );
13927 $this->_site = $site;
13928
13929 if ( ! $setup_account ) {
13930 $this->_store_site();
13931
13932 $this->sync_plan_if_not_exist( $site->plan_id );
13933
13934 if ( ! empty( $license_key ) && FS_Plugin_License::is_valid_id( $site->license_id ) ) {
13935 $this->sync_license_if_not_exist( $site->license_id, $license_key );
13936 }
13937
13938 $this->_admin_notices->remove_sticky( 'connect_account', false );
13939
13940 return $site;
13941 }
13942
13943 return $this->setup_account( $this->_user, $this->_site, $redirect );
13944 } else {
13945 $installs = array();
13946 foreach ( $result->installs as $install ) {
13947 $installs[] = new FS_Site( $install );
13948 }
13949
13950 return $this->setup_network_account(
13951 $user,
13952 $installs,
13953 $redirect
13954 );
13955 }
13956 }
13957
13958 /**
13959 * Initiate an API request to create a collection of installs.
13960 *
13961 * @author Vova Feldman (@svovaf)
13962 * @since 2.0.0
13963 *
13964 * @param \FS_User $user
13965 * @param bool $license_key
13966 * @param bool $trial_plan_id
13967 * @param array $sites
13968 * @param bool $redirect
13969 * @param bool $silent
13970 *
13971 * @return object|mixed
13972 */
13973 private function create_installs_with_user(
13974 FS_User $user,
13975 $license_key = false,
13976 $trial_plan_id = false,
13977 $sites = array(),
13978 $redirect = false,
13979 $silent = false
13980 ) {
13981 $extra_install_params = array(
13982 'uid' => $this->get_anonymous_id(),
13983 'is_disconnected' => false,
13984 );
13985
13986 if ( ! empty( $license_key ) ) {
13987 $extra_install_params['license_key'] = $this->apply_filters( 'license_key', $license_key );
13988 } else if ( FS_Plugin_Plan::is_valid_id( $trial_plan_id ) ) {
13989 $extra_install_params['trial_plan_id'] = $trial_plan_id;
13990 }
13991
13992 if ( ! empty( $sites ) ) {
13993 $extra_install_params['sites'] = $sites;
13994 }
13995
13996 $args = $this->get_install_data_for_api( $extra_install_params, false, false );
13997
13998 // Install the plugin.
13999 $result = $this->get_api_user_scope_by_user( $user )->call(
14000 "/plugins/{$this->get_id()}/installs.json",
14001 'post',
14002 $args
14003 );
14004
14005 if ( ! $this->is_api_result_entity( $result ) &&
14006 ! $this->is_api_result_object( $result, 'installs' )
14007 ) {
14008 if ( ! empty( $args['license_key'] ) ) {
14009 // Pass full the fully entered license key to the failure handler.
14010 $args['license_key'] = $license_key;
14011 }
14012
14013 $result = $this->apply_filters( 'after_install_failure', $result, $args );
14014
14015 if ( ! $silent ) {
14016 $this->_admin_notices->add(
14017 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
14018 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $result->error->message . '</b>',
14019 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
14020 'error'
14021 );
14022 }
14023
14024 if ( $redirect ) {
14025 /**
14026 * We set the user before getting the user scope API handler, so the user became temporarily
14027 * registered (`is_registered() = true`). Since the API returned an error and we will redirect,
14028 * we have to set the user to `null`, otherwise, the user will be redirected to the wrong
14029 * activation page based on the return value of `is_registered()`. In addition, in case the
14030 * context plugin doesn't have a settings menu and the default page is the `Plugins` page,
14031 * misleading plugin activation errors will be shown on the `Plugins` page.
14032 *
14033 * @author Leo Fajardo (@leorw)
14034 */
14035 $this->_user = null;
14036
14037 fs_redirect( $this->get_activation_url( array( 'error' => $result->error->message ) ) );
14038 }
14039 }
14040
14041 return $result;
14042 }
14043
14044 /**
14045 * Tries to activate add-on account based on parent plugin info.
14046 *
14047 * @author Vova Feldman (@svovaf)
14048 * @since 1.0.6
14049 *
14050 * @param Freemius $parent_fs
14051 */
14052 private function _activate_addon_account( Freemius $parent_fs ) {
14053 if ( $this->is_registered() ) {
14054 // Already activated.
14055 return;
14056 }
14057
14058 // Activate add-on with parent plugin credentials.
14059 $addon_install = $parent_fs->get_api_site_scope()->call(
14060 "/addons/{$this->_plugin->id}/installs.json",
14061 'post',
14062 $this->get_install_data_for_api( array(
14063 'uid' => $this->get_anonymous_id(),
14064 ), false, false )
14065 );
14066
14067 if ( isset( $addon_install->error ) ) {
14068 $this->_admin_notices->add(
14069 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
14070 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $addon_install->error->message . '</b>',
14071 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
14072 'error'
14073 );
14074
14075 return;
14076 }
14077
14078 // Get user information based on parent's plugin.
14079 $user = $parent_fs->get_user();
14080
14081 // First of all, set site and user info - otherwise we won't
14082 // be able to invoke API calls.
14083 $this->_site = new FS_Site( $addon_install );
14084 $this->_user = $user;
14085
14086 // Sync add-on plans.
14087 $this->_sync_plans();
14088
14089 // Get site's current plan.
14090 //$this->_site->plan = $this->_get_plan_by_id( $this->_site->plan->id );
14091
14092 $this->_set_account( $user, $this->_site );
14093
14094 // Sync licenses.
14095 $this->_sync_licenses();
14096
14097 // Try to activate premium license.
14098 $this->_activate_license( true );
14099 }
14100
14101 /**
14102 * Tries to activate parent account based on add-on's info.
14103 *
14104 * @author Vova Feldman (@svovaf)
14105 * @since 1.2.2.7
14106 *
14107 * @param Freemius $parent_fs
14108 */
14109 private function activate_parent_account( Freemius $parent_fs ) {
14110 if ( ! $this->is_addon() ) {
14111 // This is not an add-on.
14112 return;
14113 }
14114
14115 if ( $parent_fs->is_registered() ) {
14116 // Already activated.
14117 return;
14118 }
14119
14120 // Activate parent with add-on's user credentials.
14121 $parent_install = $this->get_api_user_scope()->call(
14122 "/plugins/{$parent_fs->_plugin->id}/installs.json",
14123 'post',
14124 $parent_fs->get_install_data_for_api( array(
14125 'uid' => $parent_fs->get_anonymous_id(),
14126 ), false, false )
14127 );
14128
14129 if ( isset( $parent_install->error ) ) {
14130 $this->_admin_notices->add(
14131 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
14132 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $parent_install->error->message . '</b>',
14133 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
14134 'error'
14135 );
14136
14137 return;
14138 }
14139
14140 $parent_fs->_admin_notices->remove_sticky( 'connect_account' );
14141
14142 if ( $parent_fs->is_pending_activation() ) {
14143 $parent_fs->_admin_notices->remove_sticky( 'activation_pending' );
14144
14145 unset( $parent_fs->_storage->is_pending_activation );
14146 }
14147
14148 // First of all, set site info - otherwise we won't
14149 // be able to invoke API calls.
14150 $parent_fs->_site = new FS_Site( $parent_install );
14151
14152 // Sync add-on plans.
14153 $parent_fs->_sync_plans();
14154
14155 // Get user information based on parent's plugin.
14156 $user = $this->get_user();
14157
14158 $parent_fs->_set_account( $user, $parent_fs->_site );
14159 }
14160
14161 #endregion
14162
14163 #----------------------------------------------------------------------------------
14164 #region Admin Menu Items
14165 #----------------------------------------------------------------------------------
14166
14167 private $_menu_items = array();
14168
14169 /**
14170 * @author Vova Feldman (@svovaf)
14171 * @since 1.2.1.8
14172 *
14173 * @return array
14174 */
14175 function get_menu_items() {
14176 return $this->_menu_items;
14177 }
14178
14179 /**
14180 * @author Vova Feldman (@svovaf)
14181 * @since 1.0.7
14182 *
14183 * @return string
14184 */
14185 function get_menu_slug() {
14186 return $this->_menu->get_slug();
14187 }
14188
14189 /**
14190 * @author Vova Feldman (@svovaf)
14191 * @since 1.0.9
14192 */
14193 function _prepare_admin_menu() {
14194 // if ( ! $this->is_on() ) {
14195 // return;
14196 // }
14197
14198 /**
14199 * When running from a site admin with a network activated module and the connection
14200 * was NOT delegated and the user still haven't skipped or opted-in, then hide the
14201 * site level settings.
14202 *
14203 * @author Vova Feldman (@svovaf)
14204 * @since 2.0.0
14205 */
14206 $should_hide_site_admin_settings = (
14207 $this->_is_network_active &&
14208 ! fs_is_network_admin() &&
14209 ! $this->is_delegated_connection() &&
14210 ! $this->is_anonymous() &&
14211 ! $this->is_registered()
14212 );
14213
14214 if ( ( ! $this->has_api_connectivity() && ! $this->is_enable_anonymous() ) ||
14215 $should_hide_site_admin_settings
14216 ) {
14217 $this->_menu->remove_menu_item( $should_hide_site_admin_settings );
14218 } else {
14219 $this->do_action( fs_is_network_admin() ?
14220 'before_network_admin_menu_init' :
14221 'before_admin_menu_init'
14222 );
14223
14224 $this->add_menu_action();
14225
14226 $this->add_network_menu_when_missing();
14227
14228 $this->add_submenu_items();
14229 }
14230 }
14231
14232 /**
14233 * Admin dashboard menu items modifications.
14234 *
14235 * NOTE: admin_menu action executed before admin_init.
14236 *
14237 * @author Vova Feldman (@svovaf)
14238 * @since 1.0.7
14239 *
14240 */
14241 private function add_menu_action() {
14242 if ( $this->is_activation_mode() ) {
14243 if ( $this->is_plugin() || ( $this->has_settings_menu() && ! $this->is_free_wp_org_theme() ) ) {
14244 $this->override_plugin_menu_with_activation();
14245 } else {
14246 /**
14247 * Handle theme opt-in when the opt-in form shows as a dialog box in the themes page.
14248 */
14249 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) ) {
14250 add_action( 'load-themes.php', array( &$this, '_install_with_current_user' ) );
14251 } else if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ||
14252 fs_request_get_bool( 'pending_activation' )
14253 ) {
14254 add_action( 'load-themes.php', array( &$this, '_install_with_new_user' ) );
14255 }
14256 }
14257 } else {
14258 if ( ! $this->is_registered() ) {
14259 // If not registered try to install user.
14260 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) {
14261 $this->_install_with_new_user();
14262 }
14263 } else if (
14264 fs_request_is_action( 'sync_user' ) &&
14265 ( ! $this->has_settings_menu() || $this->is_free_wp_org_theme() )
14266 ) {
14267 $this->_handle_account_user_sync();
14268 }
14269 }
14270 }
14271
14272 /**
14273 * @author Vova Feldman (@svovaf)
14274 * @since 1.0.1
14275 */
14276 function _redirect_on_clicked_menu_link() {
14277 $this->_logger->entrance();
14278
14279 $page = strtolower( isset( $_REQUEST['page'] ) ? $_REQUEST['page'] : '' );
14280
14281 $this->_logger->log( 'page = ' . $page );
14282
14283 foreach ( $this->_menu_items as $priority => $items ) {
14284 foreach ( $items as $item ) {
14285 if ( isset( $item['url'] ) ) {
14286 if ( $page === $this->_menu->get_slug( strtolower( $item['menu_slug'] ) ) ) {
14287 $this->_logger->log( 'Redirecting to ' . $item['url'] );
14288
14289 fs_redirect( $item['url'] );
14290 }
14291 }
14292 }
14293 }
14294 }
14295
14296 /**
14297 * Remove plugin's all admin menu items & pages, and replace with activation page.
14298 *
14299 * @author Vova Feldman (@svovaf)
14300 * @since 1.0.1
14301 */
14302 private function override_plugin_menu_with_activation() {
14303 $this->_logger->entrance();
14304
14305 $hook = false;
14306
14307 if ( ! $this->_menu->has_menu() ) {
14308 // Add the opt-in page without a menu item.
14309 $hook = FS_Admin_Menu_Manager::add_subpage(
14310 null,
14311 $this->get_plugin_name(),
14312 $this->get_plugin_name(),
14313 'manage_options',
14314 $this->_slug,
14315 array( &$this, '_connect_page_render' )
14316 );
14317 } else if ( $this->_menu->is_top_level() ) {
14318 if ( $this->_menu->is_override_exact() ) {
14319 // Make sure the current page is matching the activation page.
14320 if ( ! $this->is_matching_url( $this->get_activation_url() ) ) {
14321 return;
14322 }
14323 }
14324
14325 $hook = $this->_menu->override_menu_item( array( &$this, '_connect_page_render' ) );
14326
14327 if ( false === $hook ) {
14328 // Create new menu item just for the opt-in.
14329 $hook = FS_Admin_Menu_Manager::add_page(
14330 $this->get_plugin_name(),
14331 $this->get_plugin_name(),
14332 'manage_options',
14333 $this->_menu->get_slug(),
14334 array( &$this, '_connect_page_render' )
14335 );
14336 }
14337 } else {
14338 $menus = array( $this->_menu->get_parent_slug() );
14339
14340 if ( $this->_menu->is_override_exact() ) {
14341 // Make sure the current page is matching the activation page.
14342 if ( ! $this->is_matching_url( $this->get_activation_url() ) ) {
14343 return;
14344 }
14345 }
14346
14347 foreach ( $menus as $parent_slug ) {
14348 $hook = $this->_menu->override_submenu_action(
14349 $parent_slug,
14350 $this->_menu->get_raw_slug(),
14351 array( &$this, '_connect_page_render' )
14352 );
14353
14354 if ( false !== $hook ) {
14355 // Found plugin's submenu item.
14356 break;
14357 }
14358 }
14359 }
14360
14361 if ( $this->is_activation_page() ) {
14362 // Clean admin page from distracting content.
14363 self::_clean_admin_content_section();
14364 }
14365
14366 if ( false !== $hook ) {
14367 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) ) {
14368 $this->_install_with_current_user();
14369 } else if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) {
14370 $this->_install_with_new_user();
14371 }
14372 }
14373 }
14374
14375 /**
14376 * If a plugin was network activated and connected but don't have a network
14377 * level settings, then add an artificial menu item for the Account and other
14378 * Freemius settings.
14379 *
14380 * @author Vova Feldman (@svovaf)
14381 * @since 2.0.0
14382 */
14383 private function add_network_menu_when_missing() {
14384 $this->_logger->entrance();
14385
14386 if ( ! $this->_is_network_active ) {
14387 // Plugin wasn't activated on the network level.
14388 return;
14389 }
14390
14391 if ( ! fs_is_network_admin() ) {
14392 // The context is not the network admin.
14393 return;
14394 }
14395
14396 if ( $this->_menu->has_network_menu() ) {
14397 // Plugin already has a network level menu.
14398 return;
14399 }
14400
14401 if ( $this->is_network_activation_mode() ) {
14402 /**
14403 * Do not add during activation mode, otherwise, there will be duplicate menus while the opt-in
14404 * screen is being shown.
14405 *
14406 * @author Leo Fajardo (@leorw)
14407 */
14408 return;
14409 }
14410
14411 if ( ! WP_FS__SHOW_NETWORK_EVEN_WHEN_DELEGATED ) {
14412 if ( $this->is_network_delegated_connection() ) {
14413 // Super-admin delegated the connection to the site admins.
14414 return;
14415 }
14416 }
14417
14418 if ( ! $this->_menu->has_menu() || $this->_menu->is_top_level() ) {
14419 $this->_dynamically_added_top_level_page_hook_name = $this->_menu->add_page_and_update(
14420 $this->get_plugin_name(),
14421 $this->get_plugin_name(),
14422 'manage_options',
14423 $this->_menu->has_menu() ? $this->_menu->get_slug() : $this->_slug
14424 );
14425 } else {
14426 $this->_menu->add_subpage_and_update(
14427 $this->_menu->get_parent_slug(),
14428 $this->get_plugin_name(),
14429 $this->get_plugin_name(),
14430 'manage_options',
14431 $this->_menu->get_slug()
14432 );
14433 }
14434 }
14435
14436 /**
14437 * @author Leo Fajardo (@leorw)
14438 * @since 1.2.1
14439 *
14440 * return string
14441 */
14442 function get_top_level_menu_capability() {
14443 global $menu;
14444
14445 $top_level_menu_slug = $this->get_top_level_menu_slug();
14446
14447 foreach ( $menu as $menu_info ) {
14448 /**
14449 * The second element in the menu info array is the capability/role that has access to the menu and the
14450 * third element is the menu slug.
14451 */
14452 if ( $menu_info[2] === $top_level_menu_slug ) {
14453 return $menu_info[1];
14454 }
14455 }
14456
14457 return 'read';
14458 }
14459
14460 /**
14461 * @author Vova Feldman (@svovaf)
14462 * @since 1.0.0
14463 *
14464 * @return string
14465 */
14466 private function get_top_level_menu_slug() {
14467 return ( $this->is_addon() ?
14468 $this->get_parent_instance()->_menu->get_top_level_menu_slug() :
14469 $this->_menu->get_top_level_menu_slug() );
14470 }
14471
14472 /**
14473 * @author Vova Feldman (@svovaf)
14474 * @since 1.2.2.7
14475 *
14476 * @return string
14477 */
14478 function get_pricing_cta_label() {
14479 $label = $this->get_text_inline( 'Upgrade', 'upgrade' );
14480
14481 if ( $this->is_in_trial_promotion() &&
14482 ! $this->is_paying_or_trial()
14483 ) {
14484 // If running a trial promotion, modify the pricing to load the trial.
14485 $label = $this->get_text_inline( 'Start Trial', 'start-trial' );
14486 } else if ( $this->is_paying() ) {
14487 $label = $this->get_text_inline( 'Pricing', 'pricing' );
14488 }
14489
14490 return $label;
14491 }
14492
14493 /**
14494 * @author Vova Feldman (@svovaf)
14495 * @since 1.2.2.7
14496 *
14497 * @return bool
14498 */
14499 function is_pricing_page_visible() {
14500 return (
14501 // Has at least one paid plan.
14502 $this->has_paid_plan() &&
14503 // Didn't ask to hide the pricing page.
14504 $this->is_page_visible( 'pricing' ) &&
14505 // Don't have a valid active license or has more than one plan.
14506 ( ! $this->is_paying() || ! $this->is_single_plan() )
14507 );
14508 }
14509
14510 /**
14511 * Add default Freemius menu items.
14512 *
14513 * @author Vova Feldman (@svovaf)
14514 * @since 1.0.0
14515 * @since 1.2.2.7 Also add submenu items when running in a free .org theme so the tabs will be visible.
14516 */
14517 private function add_submenu_items() {
14518 $this->_logger->entrance();
14519
14520 if ( $this->is_addon() ) {
14521 // No submenu items for add-ons.
14522 $add_submenu_items = false;
14523 } else if ( $this->is_free_wp_org_theme() && ! fs_is_network_admin() ) {
14524 // Also add submenu items when running in a free .org theme so the tabs will be visible.
14525 $add_submenu_items = true;
14526 } else if ( $this->is_activation_mode() && ! $this->is_free_wp_org_theme() ) {
14527 $add_submenu_items = false;
14528 } else if ( fs_is_network_admin() ) {
14529 /**
14530 * Add submenu items to network level when plugin was network
14531 * activated and the super-admin did NOT delegated the connection
14532 * of all sites to site admins.
14533 */
14534 $add_submenu_items = (
14535 $this->_is_network_active &&
14536 ( WP_FS__SHOW_NETWORK_EVEN_WHEN_DELEGATED ||
14537 ! $this->is_network_delegated_connection() )
14538 );
14539 } else {
14540 $add_submenu_items = ( ! $this->_is_network_active || $this->is_delegated_connection() );
14541 }
14542
14543 if ( $add_submenu_items ) {
14544 if ( $this->has_affiliate_program() ) {
14545 // Add affiliation page.
14546 $this->add_submenu_item(
14547 $this->get_text_inline( 'Affiliation', 'affiliation' ),
14548 array( &$this, '_affiliation_page_render' ),
14549 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Affiliation', 'affiliation' ),
14550 'manage_options',
14551 'affiliation',
14552 'Freemius::_clean_admin_content_section',
14553 WP_FS__DEFAULT_PRIORITY,
14554 $this->is_submenu_item_visible( 'affiliation' )
14555 );
14556 }
14557
14558 if ( $this->is_registered() ) {
14559 $show_account = (
14560 $this->is_submenu_item_visible( 'account' ) &&
14561 /**
14562 * @since 1.2.2.7 Don't show the Account for free WP.org themes without any paid plans.
14563 */
14564 ( ! $this->is_free_wp_org_theme() || $this->has_paid_plan() )
14565 );
14566
14567 // Add user account page.
14568 $this->add_submenu_item(
14569 $this->get_text_inline( 'Account', 'account' ),
14570 array( &$this, '_account_page_render' ),
14571 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Account', 'account' ),
14572 'manage_options',
14573 'account',
14574 array( &$this, '_account_page_load' ),
14575 WP_FS__DEFAULT_PRIORITY,
14576 $show_account
14577 );
14578 }
14579
14580 // Add contact page.
14581 $this->add_submenu_item(
14582 $this->get_text_inline( 'Contact Us', 'contact-us' ),
14583 array( &$this, '_contact_page_render' ),
14584 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Contact Us', 'contact-us' ),
14585 'manage_options',
14586 'contact',
14587 'Freemius::_clean_admin_content_section',
14588 WP_FS__DEFAULT_PRIORITY,
14589 $this->is_submenu_item_visible( 'contact' )
14590 );
14591
14592 if ( $this->has_addons() ) {
14593 $this->add_submenu_item(
14594 $this->get_text_inline( 'Add-Ons', 'add-ons' ),
14595 array( &$this, '_addons_page_render' ),
14596 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Add-Ons', 'add-ons' ),
14597 'manage_options',
14598 'addons',
14599 array( &$this, '_addons_page_load' ),
14600 WP_FS__LOWEST_PRIORITY - 1,
14601 $this->is_submenu_item_visible( 'addons' )
14602 );
14603 }
14604
14605 $show_pricing = (
14606 $this->is_submenu_item_visible( 'pricing' ) &&
14607 $this->is_pricing_page_visible()
14608 );
14609
14610 $pricing_cta_text = $this->get_pricing_cta_label();
14611 $pricing_class = 'upgrade-mode';
14612 if ( $show_pricing ) {
14613 if ( $this->is_in_trial_promotion() &&
14614 ! $this->is_paying_or_trial()
14615 ) {
14616 // If running a trial promotion, modify the pricing to load the trial.
14617 $pricing_class = 'trial-mode';
14618 } else if ( $this->is_paying() ) {
14619 $pricing_class = '';
14620 }
14621 }
14622
14623 // Add upgrade/pricing page.
14624 $this->add_submenu_item(
14625 $pricing_cta_text . '&nbsp;&nbsp;' . ( is_rtl() ? '&#x2190;' : '&#x27a4;' ),
14626 array( &$this, '_pricing_page_render' ),
14627 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_x_inline( 'Pricing', 'noun', 'pricing' ),
14628 'manage_options',
14629 'pricing',
14630 'Freemius::_clean_admin_content_section',
14631 WP_FS__LOWEST_PRIORITY,
14632 $show_pricing,
14633 $pricing_class
14634 );
14635 }
14636
14637 if ( 0 < count( $this->_menu_items ) ) {
14638 if ( ! $this->_menu->is_top_level() ) {
14639 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
14640
14641 // Append submenu items right after the plugin's submenu item.
14642 $this->order_sub_submenu_items();
14643 } else {
14644 // Append submenu items.
14645 $this->embed_submenu_items();
14646 }
14647 }
14648 }
14649
14650 /**
14651 * Moved the actual submenu item additions to a separated function,
14652 * in order to support sub-submenu items when the plugin's settings
14653 * only have a submenu and not top-level menu item.
14654 *
14655 * @author Vova Feldman (@svovaf)
14656 * @since 1.1.4
14657 */
14658 private function embed_submenu_items() {
14659 $item_template = $this->_menu->is_top_level() ?
14660 '<span class="fs-submenu-item %s %s %s">%s</span>' :
14661 '<span class="fs-submenu-item fs-sub %s %s %s">%s</span>';
14662
14663 $top_level_menu_capability = $this->get_top_level_menu_capability();
14664
14665 ksort( $this->_menu_items );
14666
14667 $is_first_submenu_item = true;
14668
14669 foreach ( $this->_menu_items as $priority => $items ) {
14670 foreach ( $items as $item ) {
14671 if ( $item['show_submenu'] && $is_first_submenu_item ) {
14672 if ( $this->_is_network_active && ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
14673 $item['menu_slug'] = '';
14674
14675 $this->_menu->override_menu_item( $item['render_function'] );
14676 }
14677
14678 $is_first_submenu_item = false;
14679 }
14680
14681 $capability = ( ! empty( $item['capability'] ) ? $item['capability'] : $top_level_menu_capability );
14682
14683 $menu_item = sprintf(
14684 $item_template,
14685 $this->get_unique_affix(),
14686 $item['menu_slug'],
14687 ! empty( $item['class'] ) ? $item['class'] : '',
14688 $item['menu_title']
14689 );
14690
14691 $menu_slug = $this->_menu->get_slug( $item['menu_slug'] );
14692
14693 if ( ! isset( $item['url'] ) ) {
14694 $hook = FS_Admin_Menu_Manager::add_subpage(
14695 $item['show_submenu'] ?
14696 $this->get_top_level_menu_slug() :
14697 null,
14698 $item['page_title'],
14699 $menu_item,
14700 $capability,
14701 $menu_slug,
14702 $item['render_function']
14703 );
14704
14705 if ( false !== $item['before_render_function'] ) {
14706 add_action( "load-$hook", $item['before_render_function'] );
14707 }
14708 } else {
14709 FS_Admin_Menu_Manager::add_subpage(
14710 $item['show_submenu'] ?
14711 $this->get_top_level_menu_slug() :
14712 null,
14713 $item['page_title'],
14714 $menu_item,
14715 $capability,
14716 $menu_slug,
14717 array( $this, '' )
14718 );
14719 }
14720 }
14721 }
14722 }
14723
14724 /**
14725 * Re-order the submenu items so all Freemius added new submenu items
14726 * are added right after the plugin's settings submenu item.
14727 *
14728 * @author Vova Feldman (@svovaf)
14729 * @since 1.1.4
14730 */
14731 private function order_sub_submenu_items() {
14732 global $submenu;
14733
14734 $menu_slug = $this->_menu->get_top_level_menu_slug();
14735
14736 /**
14737 * Before "admin_menu" fires, WordPress will loop over the default submenus and remove pages for which the user
14738 * does not have permissions. So in case a plugin does not have top-level menu but does have submenus under any
14739 * of the default menus, only users that have the right role can access its sub-submenus (Account, Contact Us,
14740 * Support Forum, etc.) since $submenu[ $menu_slug ] will be empty if the user doesn't have permission.
14741 *
14742 * In case a plugin does not have submenus under any of the default menus but does have submenus under the menu
14743 * of another plugin, only users that have the right role can access its sub-submenus since we will use the
14744 * capability needed to access the parent menu as the capability for the submenus that we will add.
14745 */
14746 if ( empty( $submenu[ $menu_slug ] ) ) {
14747 return;
14748 }
14749
14750 $top_level_menu = &$submenu[ $menu_slug ];
14751
14752 $all_submenu_items_after = array();
14753
14754 $found_submenu_item = false;
14755
14756 foreach ( $top_level_menu as $submenu_id => $meta ) {
14757 if ( $found_submenu_item ) {
14758 // Remove all submenu items after the plugin's submenu item.
14759 $all_submenu_items_after[] = $meta;
14760 unset( $top_level_menu[ $submenu_id ] );
14761 }
14762
14763 if ( $this->_menu->get_raw_slug() === $meta[2] ) {
14764 // Found the submenu item, put all below.
14765 $found_submenu_item = true;
14766 continue;
14767 }
14768 }
14769
14770 // Embed all plugin's new submenu items.
14771 $this->embed_submenu_items();
14772
14773 // Start with specially high number to make sure it's appended.
14774 $i = max( 10000, max( array_keys( $top_level_menu ) ) + 1 );
14775 foreach ( $all_submenu_items_after as $meta ) {
14776 $top_level_menu[ $i ] = $meta;
14777 $i ++;
14778 }
14779
14780 // Sort submenu items.
14781 ksort( $top_level_menu );
14782 }
14783
14784 /**
14785 * Helper method to return the module's support forum URL.
14786 *
14787 * @author Vova Feldman (@svovaf)
14788 * @since 1.2.2.7
14789 *
14790 * @return string
14791 */
14792 function get_support_forum_url() {
14793 return $this->apply_filters( 'support_forum_url', "https://wordpress.org/support/{$this->_module_type}/{$this->_slug}" );
14794 }
14795
14796 /**
14797 * Displays the Support Forum link when enabled.
14798 *
14799 * Can be filtered like so:
14800 *
14801 * function _fs_show_support_menu( $is_visible, $menu_id ) {
14802 * if ( 'support' === $menu_id ) {
14803 * return _fs->is_registered();
14804 * }
14805 * return $is_visible;
14806 * }
14807 * _fs()->add_filter('is_submenu_visible', '_fs_show_support_menu', 10, 2);
14808 *
14809 */
14810 function _add_default_submenu_items() {
14811 if ( ! $this->is_on() ) {
14812 return;
14813 }
14814
14815 if ( ! $this->is_activation_mode() &&
14816 ( ( $this->_is_network_active && fs_is_network_admin() ) ||
14817 ( ! $this->_is_network_active && is_admin() ) )
14818 ) {
14819 $this->add_submenu_link_item(
14820 $this->apply_filters( 'support_forum_submenu', $this->get_text_inline( 'Support Forum', 'support-forum' ) ),
14821 $this->get_support_forum_url(),
14822 'wp-support-forum',
14823 null,
14824 50,
14825 $this->is_submenu_item_visible( 'support' )
14826 );
14827 }
14828 }
14829
14830 /**
14831 * @author Vova Feldman (@svovaf)
14832 * @since 1.0.1
14833 *
14834 * @param string $menu_title
14835 * @param callable $render_function
14836 * @param bool|string $page_title
14837 * @param string $capability
14838 * @param bool|string $menu_slug
14839 * @param bool|callable $before_render_function
14840 * @param int $priority
14841 * @param bool $show_submenu
14842 * @param string $class Since 1.2.1.5 can add custom classes to menu items.
14843 */
14844 function add_submenu_item(
14845 $menu_title,
14846 $render_function,
14847 $page_title = false,
14848 $capability = 'manage_options',
14849 $menu_slug = false,
14850 $before_render_function = false,
14851 $priority = WP_FS__DEFAULT_PRIORITY,
14852 $show_submenu = true,
14853 $class = ''
14854 ) {
14855 $this->_logger->entrance( 'Title = ' . $menu_title );
14856
14857 if ( $this->is_addon() ) {
14858 $parent_fs = $this->get_parent_instance();
14859
14860 if ( is_object( $parent_fs ) ) {
14861 $parent_fs->add_submenu_item(
14862 $menu_title,
14863 $render_function,
14864 $page_title,
14865 $capability,
14866 $menu_slug,
14867 $before_render_function,
14868 $priority,
14869 $show_submenu,
14870 $class
14871 );
14872
14873 return;
14874 }
14875 }
14876
14877 if ( ! isset( $this->_menu_items[ $priority ] ) ) {
14878 $this->_menu_items[ $priority ] = array();
14879 }
14880
14881 $this->_menu_items[ $priority ][] = array(
14882 'page_title' => is_string( $page_title ) ? $page_title : $menu_title,
14883 'menu_title' => $menu_title,
14884 'capability' => $capability,
14885 'menu_slug' => is_string( $menu_slug ) ? $menu_slug : strtolower( $menu_title ),
14886 'render_function' => $render_function,
14887 'before_render_function' => $before_render_function,
14888 'show_submenu' => $show_submenu,
14889 'class' => $class,
14890 );
14891 }
14892
14893 /**
14894 * @author Vova Feldman (@svovaf)
14895 * @since 1.0.1
14896 *
14897 * @param string $menu_title
14898 * @param string $url
14899 * @param bool $menu_slug
14900 * @param string $capability
14901 * @param int $priority
14902 * @param bool $show_submenu
14903 */
14904 function add_submenu_link_item(
14905 $menu_title,
14906 $url,
14907 $menu_slug = false,
14908 $capability = 'read',
14909 $priority = WP_FS__DEFAULT_PRIORITY,
14910 $show_submenu = true
14911 ) {
14912 $this->_logger->entrance( 'Title = ' . $menu_title . '; Url = ' . $url );
14913
14914 if ( $this->is_addon() ) {
14915 $parent_fs = $this->get_parent_instance();
14916
14917 if ( is_object( $parent_fs ) ) {
14918 $parent_fs->add_submenu_link_item(
14919 $menu_title,
14920 $url,
14921 $menu_slug,
14922 $capability,
14923 $priority,
14924 $show_submenu
14925 );
14926
14927 return;
14928 }
14929 }
14930
14931 if ( ! isset( $this->_menu_items[ $priority ] ) ) {
14932 $this->_menu_items[ $priority ] = array();
14933 }
14934
14935 $this->_menu_items[ $priority ][] = array(
14936 'menu_title' => $menu_title,
14937 'capability' => $capability,
14938 'menu_slug' => is_string( $menu_slug ) ? $menu_slug : strtolower( $menu_title ),
14939 'url' => $url,
14940 'page_title' => $menu_title,
14941 'render_function' => 'fs_dummy',
14942 'before_render_function' => '',
14943 'show_submenu' => $show_submenu,
14944 );
14945 }
14946
14947 #endregion ------------------------------------------------------------------
14948
14949
14950 #--------------------------------------------------------------------------------
14951 #region Actions / Hooks / Filters
14952 #--------------------------------------------------------------------------------
14953
14954 /**
14955 * @author Vova Feldman (@svovaf)
14956 * @since 1.1.7
14957 *
14958 * @param string $tag
14959 *
14960 * @return string
14961 */
14962 public function get_action_tag( $tag ) {
14963 return self::get_action_tag_static( $tag, $this->_slug, $this->is_plugin() );
14964 }
14965
14966 /**
14967 * @author Vova Feldman (@svovaf)
14968 * @since 1.2.1.6
14969 *
14970 * @param string $tag
14971 * @param string $slug
14972 * @param bool $is_plugin
14973 *
14974 * @return string
14975 */
14976 static function get_action_tag_static( $tag, $slug = '', $is_plugin = true ) {
14977 $action = "fs_{$tag}";
14978
14979 if ( ! empty( $slug ) ) {
14980 $action .= '_' . self::get_module_unique_affix( $slug, $is_plugin );
14981 }
14982
14983 return $action;
14984 }
14985
14986 /**
14987 * Returns a string that can be used to generate a unique action name,
14988 * option name, HTML element ID, or HTML element class.
14989 *
14990 * @author Leo Fajardo (@leorw)
14991 * @since 1.2.2
14992 *
14993 * @return string
14994 */
14995 public function get_unique_affix() {
14996 return self::get_module_unique_affix( $this->_slug, $this->is_plugin() );
14997 }
14998
14999 /**
15000 * Returns a string that can be used to generate a unique action name,
15001 * option name, HTML element ID, or HTML element class.
15002 *
15003 * @author Vova Feldman (@svovaf)
15004 * @since 1.2.2.5
15005 *
15006 * @param string $slug
15007 * @param bool $is_plugin
15008 *
15009 * @return string
15010 */
15011 static function get_module_unique_affix( $slug, $is_plugin = true ) {
15012 $affix = $slug;
15013
15014 if ( ! $is_plugin ) {
15015 $affix .= '-' . WP_FS__MODULE_TYPE_THEME;
15016 }
15017
15018 return $affix;
15019 }
15020
15021 /**
15022 * @author Vova Feldman (@svovaf)
15023 * @since 1.2.1
15024 * @since 1.2.2.5 The AJAX action names are based on the module ID, not like the non-AJAX actions that are
15025 * based on the slug for backward compatibility.
15026 *
15027 * @param string $tag
15028 *
15029 * @return string
15030 */
15031 function get_ajax_action( $tag ) {
15032 return self::get_ajax_action_static( $tag, $this->_module_id );
15033 }
15034
15035 /**
15036 * @author Vova Feldman (@svovaf)
15037 * @since 1.2.1.7
15038 *
15039 * @param string $tag
15040 *
15041 * @return string
15042 */
15043 function get_ajax_security( $tag ) {
15044 return wp_create_nonce( $this->get_ajax_action( $tag ) );
15045 }
15046
15047 /**
15048 * @author Vova Feldman (@svovaf)
15049 * @since 1.2.1.7
15050 *
15051 * @param string $tag
15052 */
15053 function check_ajax_referer( $tag ) {
15054 check_ajax_referer( $this->get_ajax_action( $tag ), 'security' );
15055 }
15056
15057 /**
15058 * @author Vova Feldman (@svovaf)
15059 * @since 1.2.1.6
15060 * @since 1.2.2.5 The AJAX action names are based on the module ID, not like the non-AJAX actions that are
15061 * based on the slug for backward compatibility.
15062 *
15063 * @param string $tag
15064 * @param number|null $module_id
15065 *
15066 * @return string
15067 */
15068 private static function get_ajax_action_static( $tag, $module_id = null ) {
15069 $action = "fs_{$tag}";
15070
15071 if ( ! empty( $module_id ) ) {
15072 $action .= "_{$module_id}";
15073 }
15074
15075 return $action;
15076 }
15077
15078 /**
15079 * Do action, specific for the current context plugin.
15080 *
15081 * @author Vova Feldman (@svovaf)
15082 * @since 1.0.1
15083 *
15084 * @param string $tag The name of the action to be executed.
15085 * @param mixed $arg,... Optional. Additional arguments which are passed on to the
15086 * functions hooked to the action. Default empty.
15087 *
15088 * @uses do_action()
15089 */
15090 function do_action( $tag, $arg = '' ) {
15091 $this->_logger->entrance( $tag );
15092
15093 $args = func_get_args();
15094
15095 call_user_func_array( 'do_action', array_merge(
15096 array( $this->get_action_tag( $tag ) ),
15097 array_slice( $args, 1 ) )
15098 );
15099 }
15100
15101 /**
15102 * Add action, specific for the current context plugin.
15103 *
15104 * @author Vova Feldman (@svovaf)
15105 * @since 1.0.1
15106 *
15107 * @param string $tag
15108 * @param callable $function_to_add
15109 * @param int $priority
15110 * @param int $accepted_args
15111 *
15112 * @uses add_action()
15113 */
15114 function add_action(
15115 $tag,
15116 $function_to_add,
15117 $priority = WP_FS__DEFAULT_PRIORITY,
15118 $accepted_args = 1
15119 ) {
15120 $this->_logger->entrance( $tag );
15121
15122 add_action( $this->get_action_tag( $tag ), $function_to_add, $priority, $accepted_args );
15123 }
15124
15125 /**
15126 * Add AJAX action, specific for the current context plugin.
15127 *
15128 * @author Vova Feldman (@svovaf)
15129 * @since 1.2.1
15130 *
15131 * @param string $tag
15132 * @param callable $function_to_add
15133 * @param int $priority
15134 *
15135 * @uses add_action()
15136 *
15137 * @return bool True if action added, false if no need to add the action since the AJAX call isn't matching.
15138 */
15139 function add_ajax_action(
15140 $tag,
15141 $function_to_add,
15142 $priority = WP_FS__DEFAULT_PRIORITY
15143 ) {
15144 $this->_logger->entrance( $tag );
15145
15146 return self::add_ajax_action_static(
15147 $tag,
15148 $function_to_add,
15149 $priority,
15150 $this->_module_id
15151 );
15152 }
15153
15154 /**
15155 * Add AJAX action.
15156 *
15157 * @author Vova Feldman (@svovaf)
15158 * @since 1.2.1.6
15159 *
15160 * @param string $tag
15161 * @param callable $function_to_add
15162 * @param int $priority
15163 * @param number|null $module_id
15164 *
15165 * @return bool True if action added, false if no need to add the action since the AJAX call isn't matching.
15166 * @uses add_action()
15167 *
15168 */
15169 static function add_ajax_action_static(
15170 $tag,
15171 $function_to_add,
15172 $priority = WP_FS__DEFAULT_PRIORITY,
15173 $module_id = null
15174 ) {
15175 self::$_static_logger->entrance( $tag );
15176
15177 if ( ! self::is_ajax_action_static( $tag, $module_id ) ) {
15178 return false;
15179 }
15180
15181 add_action(
15182 'wp_ajax_' . self::get_ajax_action_static( $tag, $module_id ),
15183 $function_to_add,
15184 $priority,
15185 0
15186 );
15187
15188 self::$_static_logger->info( "$tag AJAX callback action added." );
15189
15190 return true;
15191 }
15192
15193 /**
15194 * Send a JSON response back to an Ajax request.
15195 *
15196 * @author Vova Feldman (@svovaf)
15197 * @since 1.2.1.5
15198 *
15199 * @param mixed $response
15200 */
15201 static function shoot_ajax_response( $response ) {
15202 wp_send_json( $response );
15203 }
15204
15205 /**
15206 * Send a JSON response back to an Ajax request, indicating success.
15207 *
15208 * @author Vova Feldman (@svovaf)
15209 * @since 1.2.1.5
15210 *
15211 * @param mixed $data Data to encode as JSON, then print and exit.
15212 */
15213 static function shoot_ajax_success( $data = null ) {
15214 wp_send_json_success( $data );
15215 }
15216
15217 /**
15218 * Send a JSON response back to an Ajax request, indicating failure.
15219 *
15220 * @author Vova Feldman (@svovaf)
15221 * @since 1.2.1.5
15222 *
15223 * @param mixed $error Optional error message.
15224 */
15225 static function shoot_ajax_failure( $error = '' ) {
15226 $result = array( 'success' => false );
15227 if ( ! empty( $error ) ) {
15228 $result['error'] = $error;
15229 }
15230
15231 wp_send_json( $result );
15232 }
15233
15234 /**
15235 * Apply filter, specific for the current context plugin.
15236 *
15237 * @author Vova Feldman (@svovaf)
15238 * @since 1.0.9
15239 *
15240 * @param string $tag The name of the filter hook.
15241 * @param mixed $value The value on which the filters hooked to `$tag` are applied on.
15242 *
15243 * @return mixed The filtered value after all hooked functions are applied to it.
15244 *
15245 * @uses apply_filters()
15246 */
15247 function apply_filters( $tag, $value ) {
15248 $this->_logger->entrance( $tag );
15249
15250 $args = func_get_args();
15251 array_unshift( $args, $this->get_unique_affix() );
15252
15253 return call_user_func_array( 'fs_apply_filter', $args );
15254 }
15255
15256 /**
15257 * Add filter, specific for the current context plugin.
15258 *
15259 * @author Vova Feldman (@svovaf)
15260 * @since 1.0.9
15261 *
15262 * @param string $tag
15263 * @param callable $function_to_add
15264 * @param int $priority
15265 * @param int $accepted_args
15266 *
15267 * @uses add_filter()
15268 */
15269 function add_filter( $tag, $function_to_add, $priority = WP_FS__DEFAULT_PRIORITY, $accepted_args = 1 ) {
15270 $this->_logger->entrance( $tag );
15271
15272 add_filter( $this->get_action_tag( $tag ), $function_to_add, $priority, $accepted_args );
15273 }
15274
15275 /**
15276 * Check if has filter.
15277 *
15278 * @author Vova Feldman (@svovaf)
15279 * @since 1.1.4
15280 *
15281 * @param string $tag
15282 * @param callable|bool $function_to_check Optional. The callback to check for. Default false.
15283 *
15284 * @return false|int
15285 *
15286 * @uses has_filter()
15287 */
15288 function has_filter( $tag, $function_to_check = false ) {
15289 $this->_logger->entrance( $tag );
15290
15291 return has_filter( $this->get_action_tag( $tag ), $function_to_check );
15292 }
15293
15294 #endregion
15295
15296 /**
15297 * Override default i18n text phrases.
15298 *
15299 * @author Vova Feldman (@svovaf)
15300 * @since 1.1.6
15301 *
15302 * @param string[] string $key_value
15303 *
15304 * @uses fs_override_i18n()
15305 */
15306 function override_i18n( $key_value ) {
15307 fs_override_i18n( $key_value, $this->_slug );
15308 }
15309
15310 /* Account Page
15311 ------------------------------------------------------------------------------------------------------------------*/
15312 /**
15313 * Update site information.
15314 *
15315 * @author Vova Feldman (@svovaf)
15316 * @since 1.0.1
15317 *
15318 * @param bool $store Flush to Database if true.
15319 * @param null|int $network_level_or_blog_id Since 2.0.0
15320 * @param \FS_Site $site Since 2.0.0
15321 */
15322 private function _store_site( $store = true, $network_level_or_blog_id = null, FS_Site $site = null ) {
15323 $this->_logger->entrance();
15324
15325 if ( empty( $this->_site->id ) ) {
15326 $this->_logger->error( "Empty install ID, can't store site." );
15327
15328 return;
15329 }
15330
15331 $encrypted_site = clone ( is_object( $site ) ? $site : $this->_site );
15332
15333 $sites = self::get_all_sites( $this->_module_type, $network_level_or_blog_id );
15334
15335 $prev_stored_user_id = $this->_storage->get( 'prev_user_id', false, $network_level_or_blog_id );
15336
15337 if ( empty( $prev_stored_user_id ) &&
15338 $this->_user->id != $this->_site->user_id
15339 ) {
15340 /**
15341 * Store the current user ID as the previous user ID so that the previous user can be used
15342 * as the install's owner while the new owner's details are not yet available.
15343 *
15344 * This will be executed only in the `replica` site. For example, there are 2 sites, namely `original`
15345 * and `replica`, then an ownership change was initiated and completed in the `original`, the `replica`
15346 * will be using the previous user until it is updated again (e.g.: until the next clone of `original`
15347 * into `replica`.
15348 *
15349 * @author Leo Fajardo (@leorw)
15350 */
15351 $this->_storage->store( 'prev_user_id', $sites[ $this->_slug ]->user_id, $network_level_or_blog_id );
15352 }
15353
15354 $sites[ $this->_slug ] = $encrypted_site;
15355
15356 $this->set_account_option( 'sites', $sites, $store, $network_level_or_blog_id );
15357 }
15358
15359 /**
15360 * Update plugin's plans information.
15361 *
15362 * @author Vova Feldman (@svovaf)
15363 * @since 1.0.2
15364 *
15365 * @param bool $store Flush to Database if true.
15366 */
15367 private function _store_plans( $store = true ) {
15368 $this->_logger->entrance();
15369
15370 $plans = self::get_all_plans( $this->_module_type );
15371
15372 // Copy plans.
15373 $encrypted_plans = array();
15374 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
15375 $encrypted_plans[] = self::_encrypt_entity( $this->_plans[ $i ] );
15376 }
15377
15378 $plans[ $this->_slug ] = $encrypted_plans;
15379
15380 $this->set_account_option( 'plans', $plans, $store );
15381 }
15382
15383 /**
15384 * Update user's plugin licenses.
15385 *
15386 * @author Vova Feldman (@svovaf)
15387 * @since 1.0.5
15388 *
15389 * @param bool $store
15390 * @param number|bool $module_id
15391 * @param FS_Plugin_License[] $licenses
15392 */
15393 private function _store_licenses( $store = true, $module_id = false, $licenses = array() ) {
15394 $this->_logger->entrance();
15395
15396 $all_licenses = self::get_all_licenses();
15397
15398 if ( ! FS_Plugin::is_valid_id( $module_id ) ) {
15399 $module_id = $this->_module_id;
15400
15401 $user_licenses = is_array( $this->_licenses ) ?
15402 $this->_licenses :
15403 array();
15404
15405 if ( empty( $user_licenses ) ) {
15406 // If the context user doesn't have any license, don't update the licenses collection.
15407 return;
15408 }
15409
15410 $new_user_licenses_map = array();
15411 foreach ( $user_licenses as $user_license ) {
15412 $new_user_licenses_map[ $user_license->id ] = $user_license;
15413 }
15414
15415 self::store_user_id_license_ids_map( array_keys( $new_user_licenses_map ), $this->_module_id, $this->_user->id );
15416
15417 // Update user licenses.
15418 $licenses_to_update_count = count( $new_user_licenses_map );
15419 foreach ( $all_licenses[ $module_id ] as $key => $license ) {
15420 if ( 0 === $licenses_to_update_count ) {
15421 break;
15422 }
15423
15424 if ( isset( $new_user_licenses_map[ $license->id ] ) ) {
15425 // Update license.
15426 $all_licenses[ $module_id ][ $key ] = $new_user_licenses_map[ $license->id ];
15427 unset( $new_user_licenses_map[ $license->id ] );
15428
15429 $licenses_to_update_count --;
15430 }
15431 }
15432
15433 if ( ! empty( $new_user_licenses_map ) ) {
15434 // Add new licenses.
15435 $all_licenses[ $module_id ] = array_merge( array_values( $new_user_licenses_map ), $all_licenses[ $module_id ] );
15436 }
15437
15438 $licenses = $all_licenses[ $module_id ];
15439 }
15440
15441 if ( ! isset( $all_licenses[ $module_id ] ) ) {
15442 $all_licenses[ $module_id ] = array();
15443 }
15444
15445 $all_licenses[ $module_id ] = $licenses;
15446
15447 self::$_accounts->set_option( 'all_licenses', $all_licenses, $store );
15448 }
15449
15450 /**
15451 * Update user information.
15452 *
15453 * @author Vova Feldman (@svovaf)
15454 * @since 1.0.1
15455 *
15456 * @param bool $store Flush to Database if true.
15457 */
15458 private function _store_user( $store = true ) {
15459 $this->_logger->entrance();
15460
15461 if ( empty( $this->_user->id ) ) {
15462 $this->_logger->error( "Empty user ID, can't store user." );
15463
15464 return;
15465 }
15466
15467 $users = self::get_all_users();
15468 $users[ $this->_user->id ] = $this->_user;
15469 self::$_accounts->set_option( 'users', $users, $store );
15470 }
15471
15472 /**
15473 * Update new updates information.
15474 *
15475 * @author Vova Feldman (@svovaf)
15476 * @since 1.0.4
15477 *
15478 * @param FS_Plugin_Tag|null $update
15479 * @param bool $store Flush to Database if true.
15480 * @param bool|number $plugin_id
15481 */
15482 private function _store_update( $update, $store = true, $plugin_id = false ) {
15483 $this->_logger->entrance();
15484
15485 if ( $update instanceof FS_Plugin_Tag ) {
15486 $update->updated = time();
15487 }
15488
15489 if ( ! is_numeric( $plugin_id ) ) {
15490 $plugin_id = $this->_plugin->id;
15491 }
15492
15493 $updates = self::get_all_updates();
15494 $updates[ $plugin_id ] = $update;
15495 self::$_accounts->set_option( 'updates', $updates, $store );
15496 }
15497
15498 /**
15499 * Update new updates information.
15500 *
15501 * @author Vova Feldman (@svovaf)
15502 * @since 1.0.6
15503 *
15504 * @param FS_Plugin[] $plugin_addons
15505 * @param bool $store Flush to Database if true.
15506 */
15507 private function _store_addons( $plugin_addons, $store = true ) {
15508 $this->_logger->entrance();
15509
15510 $addons = self::get_all_addons();
15511 $addons[ $this->_plugin->id ] = $plugin_addons;
15512 self::$_accounts->set_option( 'addons', $addons, $store );
15513 }
15514
15515 /**
15516 * Delete plugin's associated add-ons.
15517 *
15518 * @author Vova Feldman (@svovaf)
15519 * @since 1.0.8
15520 *
15521 * @param bool $store
15522 *
15523 * @return bool
15524 */
15525 private function _delete_account_addons( $store = true ) {
15526 $all_addons = self::get_all_account_addons();
15527
15528 if ( ! isset( $all_addons[ $this->_plugin->id ] ) ) {
15529 return false;
15530 }
15531
15532 unset( $all_addons[ $this->_plugin->id ] );
15533
15534 self::$_accounts->set_option( 'account_addons', $all_addons, $store );
15535
15536 return true;
15537 }
15538
15539 /**
15540 * Update account add-ons list.
15541 *
15542 * @author Vova Feldman (@svovaf)
15543 * @since 1.0.6
15544 *
15545 * @param FS_Plugin[] $addons
15546 * @param bool $store Flush to Database if true.
15547 */
15548 private function _store_account_addons( $addons, $store = true ) {
15549 $this->_logger->entrance();
15550
15551 $all_addons = self::get_all_account_addons();
15552 $all_addons[ $this->_plugin->id ] = $addons;
15553 self::$_accounts->set_option( 'account_addons', $all_addons, $store );
15554 }
15555
15556 /**
15557 * Store account params in the Database.
15558 *
15559 * @author Vova Feldman (@svovaf)
15560 * @since 1.0.1
15561 *
15562 * @param null|int $blog_id Since 2.0.0
15563 */
15564 private function _store_account( $blog_id = null ) {
15565 $this->_logger->entrance();
15566
15567 $this->_store_site( false, $blog_id );
15568 $this->_store_user( false );
15569 $this->_store_plans( false );
15570 $this->_store_licenses( false );
15571
15572 self::$_accounts->store( $blog_id );
15573 }
15574
15575 /**
15576 * Sync user's information.
15577 *
15578 * @author Vova Feldman (@svovaf)
15579 * @since 1.0.3
15580 * @uses FS_Api
15581 */
15582 private function _handle_account_user_sync() {
15583 $this->_logger->entrance();
15584
15585 $api = $this->get_api_user_scope();
15586
15587 // Get user's information.
15588 $user = $api->get( '/', true );
15589
15590 if ( isset( $user->id ) ) {
15591 $this->_user->first = $user->first;
15592 $this->_user->last = $user->last;
15593 $this->_user->email = $user->email;
15594
15595 $is_menu_item_account_visible = $this->is_submenu_item_visible( 'account' );
15596
15597 if ( $user->is_verified &&
15598 ( ! isset( $this->_user->is_verified ) || false === $this->_user->is_verified )
15599 ) {
15600 $this->_user->is_verified = true;
15601
15602 $this->do_action( 'account_email_verified', $user->email );
15603
15604 $this->_admin_notices->add(
15605 $this->get_text_inline( 'Your email has been successfully verified - you are AWESOME!', 'email-verified-message' ),
15606 $this->get_text_x_inline( 'Right on', 'a positive response', 'right-on' ) . '!',
15607 'success',
15608 // Make admin sticky if account menu item is invisible,
15609 // since the page will be auto redirected to the plugin's
15610 // main settings page, and the non-sticky message
15611 // will disappear.
15612 ! $is_menu_item_account_visible,
15613 'email_verified'
15614 );
15615 }
15616
15617 // Flush user details to DB.
15618 $this->_store_user();
15619
15620 $this->do_action( 'after_account_user_sync', $user );
15621
15622 /**
15623 * If account menu item is hidden, redirect to plugin's main settings page.
15624 *
15625 * @author Vova Feldman (@svovaf)
15626 * @since 1.1.6
15627 *
15628 * @link https://github.com/Freemius/wordpress-sdk/issues/6
15629 */
15630 if ( ! $is_menu_item_account_visible ) {
15631 fs_redirect( $this->_get_admin_page_url() );
15632 }
15633 }
15634 }
15635
15636 /**
15637 * @author Vova Feldman (@svovaf)
15638 * @since 1.0.9
15639 * @uses FS_Api
15640 *
15641 * @param number|bool $license_id
15642 *
15643 * @return FS_Subscription|object|bool
15644 */
15645 private function _fetch_site_license_subscription( $license_id = false ) {
15646 $this->_logger->entrance();
15647 $api = $this->get_api_site_scope();
15648
15649 if ( ! is_numeric( $license_id ) ) {
15650 $license_id = $this->_license->id;
15651 }
15652
15653 $result = $api->get( "/licenses/{$license_id}/subscriptions.json", true );
15654
15655 return ! isset( $result->error ) ?
15656 ( ( is_array( $result->subscriptions ) && 0 < count( $result->subscriptions ) ) ?
15657 new FS_Subscription( $result->subscriptions[0] ) :
15658 false
15659 ) :
15660 $result;
15661 }
15662
15663 /**
15664 * @author Vova Feldman (@svovaf)
15665 * @since 1.0.4
15666 * @uses FS_Api
15667 *
15668 * @param number|bool $plan_id
15669 *
15670 * @return FS_Plugin_Plan|object
15671 */
15672 private function _fetch_site_plan( $plan_id = false ) {
15673 $this->_logger->entrance();
15674 $api = $this->get_api_site_scope();
15675
15676 if ( ! is_numeric( $plan_id ) ) {
15677 $plan_id = $this->_site->plan_id;
15678 }
15679
15680 $plan = $api->get( "/plans/{$plan_id}.json", true );
15681
15682 return ! isset( $plan->error ) ? new FS_Plugin_Plan( $plan ) : $plan;
15683 }
15684
15685 /**
15686 * @author Vova Feldman (@svovaf)
15687 * @since 1.0.5
15688 * @uses FS_Api
15689 *
15690 * @return FS_Plugin_Plan[]|object
15691 */
15692 private function _fetch_plugin_plans() {
15693 $this->_logger->entrance();
15694 $api = $this->get_current_or_network_user_api_scope();
15695
15696 /**
15697 * @since 1.2.3 When running in DEV mode, retrieve pending plans as well.
15698 */
15699 $result = $api->get( "/plugins/{$this->_module_id}/plans.json?show_pending=" . ( $this->has_secret_key() ? 'true' : 'false' ), true );
15700
15701 if ( $this->is_api_result_object( $result, 'plans' ) && is_array( $result->plans ) ) {
15702 for ( $i = 0, $len = count( $result->plans ); $i < $len; $i ++ ) {
15703 $result->plans[ $i ] = new FS_Plugin_Plan( $result->plans[ $i ] );
15704 }
15705
15706 $result = $result->plans;
15707 }
15708
15709 return $result;
15710 }
15711
15712 /**
15713 * @author Vova Feldman (@svovaf)
15714 * @since 2.0.0
15715 *
15716 * @param number $plan_id
15717 *
15718 * @return \FS_Plugin_Plan|object
15719 */
15720 private function fetch_plan_by_id( $plan_id ) {
15721 $this->_logger->entrance();
15722 $api = $this->get_current_or_network_user_api_scope();
15723
15724 $result = $api->get( "/plugins/{$this->_module_id}/plans/{$plan_id}.json", true );
15725
15726 return $this->is_api_result_entity( $result ) ?
15727 new FS_Plugin_Plan( $result ) :
15728 $result;
15729 }
15730
15731 /**
15732 * @author Vova Feldman (@svovaf)
15733 * @since 1.0.5
15734 * @uses FS_Api
15735 *
15736 * @param number|bool $plugin_id
15737 * @param number|bool $site_license_id
15738 * @param array $foreign_licenses @since 2.0.0. This is used by network-activated plugins.
15739 * @param number|null $blog_id
15740 *
15741 * @return FS_Plugin_License[]|object
15742 */
15743 private function _fetch_licenses(
15744 $plugin_id = false,
15745 $site_license_id = false,
15746 $foreign_licenses = array(),
15747 $blog_id = null
15748 ) {
15749 $this->_logger->entrance();
15750
15751 $api = $this->get_api_user_scope();
15752
15753 if ( ! is_numeric( $plugin_id ) ) {
15754 $plugin_id = $this->_plugin->id;
15755 }
15756
15757 $user_licenses_endpoint = "/plugins/{$plugin_id}/licenses.json";
15758 if ( ! empty ( $foreign_licenses ) ) {
15759 $foreign_licenses = array(
15760 // Prefix with `+` to tell the server to include foreign licenses in the licenses collection.
15761 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ),
15762 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) )
15763 );
15764
15765 $user_licenses_endpoint = add_query_arg( $foreign_licenses, $user_licenses_endpoint );
15766 }
15767
15768 $result = $api->get( $user_licenses_endpoint, true );
15769
15770 $is_site_license_synced = false;
15771
15772 $api_errors = array();
15773
15774 if ( $this->is_api_result_object( $result, 'licenses' ) &&
15775 is_array( $result->licenses )
15776 ) {
15777 for ( $i = 0, $len = count( $result->licenses ); $i < $len; $i ++ ) {
15778 $result->licenses[ $i ] = new FS_Plugin_License( $result->licenses[ $i ] );
15779
15780 if ( ( ! $is_site_license_synced ) && is_numeric( $site_license_id ) ) {
15781 $is_site_license_synced = ( $site_license_id == $result->licenses[ $i ]->id );
15782 }
15783 }
15784
15785 $result = $result->licenses;
15786 } else {
15787 $api_errors[] = $result;
15788 $result = array();
15789 }
15790
15791 if ( ! $is_site_license_synced ) {
15792 if ( ! is_null( $blog_id ) ) {
15793 /**
15794 * If blog ID is not null, the request is for syncing of the license of a single site via the
15795 * network-level "Account" page.
15796 *
15797 * @author Leo Fajardo (@leorw)
15798 */
15799 $this->switch_to_blog( $blog_id );
15800 }
15801
15802 $api = $this->get_api_site_scope();
15803
15804 if ( is_numeric( $site_license_id ) ) {
15805 // Try to retrieve a foreign license that is linked to the install.
15806 $api_result = $api->call( '/licenses.json' );
15807
15808 if ( $this->is_api_result_object( $api_result, 'licenses' ) &&
15809 is_array( $api_result->licenses )
15810 ) {
15811 $licenses = $api_result->licenses;
15812
15813 if ( ! empty( $licenses ) ) {
15814 $result[] = new FS_Plugin_License( $licenses[0] );
15815 }
15816 } else {
15817 $api_errors[] = $api_result;
15818 }
15819 } else if ( is_object( $this->_license ) ) {
15820 $is_license_in_result = false;
15821 if ( ! empty( $result ) ) {
15822 foreach ( $result as $license ) {
15823 if ( $license->id == $this->_license->id ) {
15824 $is_license_in_result = true;
15825 break;
15826 }
15827 }
15828 }
15829
15830 if ( ! $is_license_in_result ) {
15831 // Fetch foreign license by ID and license key.
15832 $license = $api->get( "/licenses/{$this->_license->id}.json?license_key=" .
15833 urlencode( $this->_license->secret_key ) );
15834
15835 if ( $this->is_api_result_entity( $license ) ) {
15836 $result[] = new FS_Plugin_License( $license );
15837 } else {
15838 $api_errors[] = $license;
15839 }
15840 }
15841 }
15842
15843 if ( ! is_null( $blog_id ) ) {
15844 $this->switch_to_blog( $this->_storage->network_install_blog_id );
15845 }
15846 }
15847
15848 if ( is_array( $result ) && 0 < count( $result ) ) {
15849 // If found at least one license, return license collection even if there are errors.
15850 return $result;
15851 }
15852
15853 if ( ! empty( $api_errors ) ) {
15854 // If found any errors and no licenses, return first error.
15855 return $api_errors[0];
15856 }
15857
15858 // Fallback to empty licenses list.
15859 return $result;
15860 }
15861
15862 /**
15863 * @author Vova Feldman (@svovaf)
15864 * @since 2.0.0
15865 *
15866 * @param number $license_id
15867 * @param string $license_key
15868 *
15869 * @return \FS_Plugin_License|object
15870 */
15871 private function fetch_license_by_key( $license_id, $license_key ) {
15872 $this->_logger->entrance();
15873
15874 $api = $this->get_current_or_network_user_api_scope();
15875
15876 $result = $api->get( "/licenses/{$license_id}.json?license_key=" . urlencode( $license_key ) );
15877
15878 return $this->is_api_result_entity( $result ) ?
15879 new FS_Plugin_License( $result ) :
15880 $result;
15881 }
15882
15883 /**
15884 * @author Vova Feldman (@svovaf)
15885 * @since 1.2.0
15886 * @uses FS_Api
15887 *
15888 * @param number|bool $plugin_id
15889 * @param bool $flush
15890 *
15891 * @return FS_Payment[]|object
15892 */
15893 function _fetch_payments( $plugin_id = false, $flush = false ) {
15894 $this->_logger->entrance();
15895
15896 $api = $this->get_api_user_scope();
15897
15898 if ( ! is_numeric( $plugin_id ) ) {
15899 $plugin_id = $this->_plugin->id;
15900 }
15901
15902 $result = $api->get( "/plugins/{$plugin_id}/payments.json?include_addons=true", $flush );
15903
15904 if ( ! isset( $result->error ) ) {
15905 for ( $i = 0, $len = count( $result->payments ); $i < $len; $i ++ ) {
15906 $result->payments[ $i ] = new FS_Payment( $result->payments[ $i ] );
15907 }
15908 $result = $result->payments;
15909 }
15910
15911 return $result;
15912 }
15913
15914 /**
15915 * @author Vova Feldman (@svovaf)
15916 * @since 1.2.1.5
15917 * @uses FS_Api
15918 *
15919 * @param bool $flush
15920 *
15921 * @return \FS_Billing|mixed
15922 */
15923 function _fetch_billing( $flush = false ) {
15924 require_once WP_FS__DIR_INCLUDES . '/entities/class-fs-billing.php';
15925
15926 $billing = $this->get_api_user_scope()->get( 'billing.json', $flush );
15927
15928 if ( $this->is_api_result_entity( $billing ) ) {
15929 $billing = new FS_Billing( $billing );
15930 }
15931
15932 return $billing;
15933 }
15934
15935 /**
15936 * @author Vova Feldman (@svovaf)
15937 * @since 1.0.5
15938 *
15939 * @param FS_Plugin_License[] $licenses
15940 * @param number $module_id
15941 */
15942 private function _update_licenses( $licenses, $module_id ) {
15943 $this->_logger->entrance();
15944
15945 if ( is_array( $licenses ) ) {
15946 for ( $i = 0, $len = count( $licenses ); $i < $len; $i ++ ) {
15947 $licenses[ $i ]->updated = time();
15948 }
15949 }
15950
15951 $this->_store_licenses( true, $module_id, $licenses );
15952 }
15953
15954 /**
15955 * @author Vova Feldman (@svovaf)
15956 * @since 1.0.4
15957 *
15958 * @param bool|number $plugin_id
15959 * @param bool $flush Since 1.1.7.3
15960 * @param int $expiration Since 1.2.2.7
15961 *
15962 * @return object|false New plugin tag info if exist.
15963 */
15964 private function _fetch_newer_version( $plugin_id = false, $flush = true, $expiration = WP_FS__TIME_24_HOURS_IN_SEC ) {
15965 $latest_tag = $this->_fetch_latest_version( $plugin_id, $flush, $expiration );
15966
15967 if ( ! is_object( $latest_tag ) ) {
15968 return false;
15969 }
15970
15971 // Check if version is actually newer.
15972 $has_new_version =
15973 // If it's an non-installed add-on then always return latest.
15974 ( $this->_is_addon_id( $plugin_id ) && ! $this->is_addon_activated( $plugin_id ) ) ||
15975 // Compare versions.
15976 version_compare( $this->get_plugin_version(), $latest_tag->version, '<' );
15977
15978 $this->_logger->departure( $has_new_version ? 'Found newer plugin version ' . $latest_tag->version : 'No new version' );
15979
15980 return $has_new_version ? $latest_tag : false;
15981 }
15982
15983 /**
15984 * @author Vova Feldman (@svovaf)
15985 * @since 1.0.5
15986 *
15987 * @param bool|number $plugin_id
15988 * @param bool $flush Since 1.1.7.3
15989 * @param int $expiration Since 1.2.2.7
15990 *
15991 * @return bool|FS_Plugin_Tag
15992 */
15993 function get_update( $plugin_id = false, $flush = true, $expiration = WP_FS__TIME_24_HOURS_IN_SEC ) {
15994 $this->_logger->entrance();
15995
15996 if ( ! is_numeric( $plugin_id ) ) {
15997 $plugin_id = $this->_plugin->id;
15998 }
15999
16000 $this->check_updates( true, $plugin_id, $flush, $expiration );
16001 $updates = $this->get_all_updates();
16002
16003 return isset( $updates[ $plugin_id ] ) && is_object( $updates[ $plugin_id ] ) ? $updates[ $plugin_id ] : false;
16004 }
16005
16006 /**
16007 * Check if site assigned with active license.
16008 *
16009 * @author Vova Feldman (@svovaf)
16010 * @since 1.0.6
16011 *
16012 * @deprecated Please use has_active_valid_license() instead because license can be cancelled.
16013 */
16014 function has_active_license() {
16015 return (
16016 is_object( $this->_license ) &&
16017 is_numeric( $this->_license->id ) &&
16018 ! $this->_license->is_expired()
16019 );
16020 }
16021
16022 /**
16023 * Check if site assigned with active & valid (not expired) license.
16024 *
16025 * @author Vova Feldman (@svovaf)
16026 * @since 1.2.1
16027 */
16028 function has_active_valid_license() {
16029 return (
16030 is_object( $this->_license ) &&
16031 is_numeric( $this->_license->id ) &&
16032 $this->_license->is_active() &&
16033 $this->_license->is_valid()
16034 );
16035 }
16036
16037 /**
16038 * Check if site assigned with license with enabled features.
16039 *
16040 * @author Vova Feldman (@svovaf)
16041 * @since 1.0.6
16042 *
16043 * @return bool
16044 */
16045 function has_features_enabled_license() {
16046 return (
16047 is_object( $this->_license ) &&
16048 is_numeric( $this->_license->id ) &&
16049 $this->_license->is_features_enabled()
16050 );
16051 }
16052
16053 /**
16054 * Check if user is a trial or have feature enabled license.
16055 *
16056 * @author Vova Feldman (@svovaf)
16057 * @since 1.1.7
16058 *
16059 * @return bool
16060 */
16061 function can_use_premium_code() {
16062 return $this->is_trial() || $this->has_features_enabled_license();
16063 }
16064
16065 /**
16066 * Checks if the current user can activate plugins or switch themes. Note that this method should only be used
16067 * after the `init` action is triggered because it is using `current_user_can()` which is only functional after
16068 * the context user is authenticated.
16069 *
16070 * @author Leo Fajardo (@leorw)
16071 * @since 1.2.2
16072 *
16073 * @return bool
16074 */
16075 function is_user_admin() {
16076 /**
16077 * Require a super-admin when network activated, running from the network level OR if
16078 * running from the site level but not delegated the opt-in.
16079 *
16080 * @author Vova Feldman (@svovaf)
16081 * @since 2.0.0
16082 */
16083 if ( $this->_is_network_active &&
16084 ( fs_is_network_admin() || ! $this->is_delegated_connection() )
16085 ) {
16086 return is_super_admin();
16087 }
16088
16089 return ( $this->is_plugin() && current_user_can( is_multisite() ? 'manage_options' : 'activate_plugins' ) )
16090 || ( $this->is_theme() && current_user_can( 'switch_themes' ) );
16091 }
16092
16093 /**
16094 * Sync site's plan.
16095 *
16096 * @author Vova Feldman (@svovaf)
16097 * @since 1.0.3
16098 *
16099 * @uses FS_Api
16100 *
16101 * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by
16102 * the admin.
16103 * @param bool $is_context_single_site @since 2.0.0. This is used when syncing a license for a single install from the
16104 * network-level "Account" page.
16105 */
16106 private function _sync_license( $background = false, $is_context_single_site = false ) {
16107 $this->_logger->entrance();
16108
16109 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
16110
16111 $is_addon_sync = ( ! $this->_plugin->is_addon() && $plugin_id != $this->get_id() );
16112
16113 if ( $is_addon_sync ) {
16114 $this->_sync_addon_license( $plugin_id, $background );
16115 } else {
16116 $this->_sync_plugin_license( $background, true, $is_context_single_site );
16117 }
16118
16119 $this->do_action( 'after_account_plan_sync', $this->get_plan_name() );
16120 }
16121
16122 /**
16123 * Sync plugin's add-on license.
16124 *
16125 * @author Vova Feldman (@svovaf)
16126 * @since 1.0.6
16127 * @uses FS_Api
16128 *
16129 * @param number $addon_id
16130 * @param bool $background
16131 */
16132 private function _sync_addon_license( $addon_id, $background ) {
16133 $this->_logger->entrance();
16134
16135 if ( $this->is_addon_activated( $addon_id ) ) {
16136 // If already installed, use add-on sync.
16137 $fs_addon = self::get_instance_by_id( $addon_id );
16138 $fs_addon->_sync_license( $background );
16139
16140 return;
16141 }
16142
16143 // Validate add-on exists.
16144 $addon = $this->get_addon( $addon_id );
16145
16146 if ( ! is_object( $addon ) ) {
16147 return;
16148 }
16149
16150 // Add add-on into account add-ons.
16151 $account_addons = $this->get_account_addons();
16152 if ( ! is_array( $account_addons ) ) {
16153 $account_addons = array();
16154 }
16155 $account_addons[] = $addon->id;
16156 $account_addons = array_unique( $account_addons );
16157 $this->_store_account_addons( $account_addons );
16158
16159 // Load add-on licenses.
16160 $licenses = $this->_fetch_licenses( $addon->id );
16161
16162 // Sync add-on licenses.
16163 if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) {
16164 $this->_update_licenses( $licenses, $addon->id );
16165
16166 if ( ! $this->is_addon_installed( $addon->id ) && FS_License_Manager::has_premium_license( $licenses ) ) {
16167 $plans_result = $this->get_api_site_or_plugin_scope()->get( "/addons/{$addon_id}/plans.json" );
16168
16169 if ( ! isset( $plans_result->error ) ) {
16170 $plans = array();
16171 foreach ( $plans_result->plans as $plan ) {
16172 $plans[] = new FS_Plugin_Plan( $plan );
16173 }
16174
16175 $this->_admin_notices->add_sticky(
16176 sprintf(
16177 ( FS_Plan_Manager::instance()->has_free_plan( $plans ) ?
16178 $this->get_text_inline( 'Your %s Add-on plan was successfully upgraded.', 'addon-successfully-upgraded-message' ) :
16179 /* translators: %s:product name, e.g. Facebook add-on was successfully... */
16180 $this->get_text_inline( '%s Add-on was successfully purchased.', 'addon-successfully-purchased-message' ) ),
16181 $addon->title
16182 ) . ' ' . $this->get_latest_download_link(
16183 $this->get_text_inline( 'Download the latest version', 'download-latest-version' ),
16184 $addon_id
16185 ),
16186 'addon_plan_upgraded_' . $addon->slug,
16187 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
16188 );
16189 }
16190 }
16191 }
16192 }
16193
16194 /**
16195 * Sync site's plugin plan.
16196 *
16197 * @author Vova Feldman (@svovaf)
16198 * @since 1.0.6
16199 * @uses FS_Api
16200 *
16201 * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by the admin.
16202 * @param bool $send_installs_update Since 2.0.0
16203 * @param bool $is_context_single_site Since 2.0.0. This is used when sending an update for a single install and
16204 * syncing its license from the network-level "Account" page (e.g.: after
16205 * activating a license only for the single install).
16206 */
16207 private function _sync_plugin_license(
16208 $background = false,
16209 $send_installs_update = true,
16210 $is_context_single_site = false
16211 ) {
16212 $this->_logger->entrance();
16213
16214 $plan_change = 'none';
16215
16216 $is_site_level_sync = ( $is_context_single_site || fs_is_blog_admin() || ! $this->_is_network_active );
16217
16218 if ( ! $send_installs_update ) {
16219 $site = $this->_site;
16220 } else {
16221 /**
16222 * Sync site info.
16223 *
16224 * @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.
16225 */
16226 if ( $is_site_level_sync ) {
16227 $result = $this->send_install_update( array(), true );
16228 $is_valid = $this->is_api_result_entity( $result );
16229 } else {
16230 $result = $this->send_installs_update( array(), true );
16231 $is_valid = $this->is_api_result_object( $result, 'installs' );
16232 }
16233
16234 if ( ! $is_valid ) {
16235 if ( $is_context_single_site ) {
16236 // Switch back to the main blog so that the following logic will have the right entities.
16237 $this->switch_to_blog( $this->_storage->network_install_blog_id );
16238 }
16239
16240 // Show API messages only if not background sync or if paying customer.
16241 if ( ! $background || $this->is_paying() ) {
16242 // Try to ping API to see if not blocked.
16243 if ( ! FS_Api::test() ) {
16244 /**
16245 * Failed to ping API - blocked!
16246 *
16247 * @author Vova Feldman (@svovaf)
16248 * @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.
16249 */
16250 $api = $this->get_api_site_scope();
16251
16252 if ( ! self::$_global_admin_notices->has_sticky( 'api_blocked' ) ) {
16253 self::$_global_admin_notices->add(
16254 sprintf(
16255 $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' ),
16256 $this->get_plugin_name(),
16257 '<a href="' . $api->get_url() . '" target="_blank">' . $api->get_url() . '</a>'
16258 ) . '<br> ' . $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . var_export( $result->error, true ),
16259 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
16260 'error',
16261 $background,
16262 'api_blocked'
16263 );
16264 }
16265 } else {
16266 // Authentication params are broken.
16267 $this->_admin_notices->add(
16268 $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' ),
16269 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
16270 'error'
16271 );
16272 }
16273 }
16274
16275 // No reason to continue with license sync while there are API issues.
16276 return;
16277 }
16278
16279 if ( $is_site_level_sync ) {
16280 $site = new FS_Site( $result );
16281 } else {
16282 // Map site addresses to their blog IDs.
16283 $address_to_blog_map = $this->get_address_to_blog_map();
16284
16285 // Find the current context install.
16286 $site = null;
16287 foreach ( $result->installs as $install ) {
16288 if ( $install->id == $this->_site->id ) {
16289 $site = new FS_Site( $install );
16290 } else {
16291 $address = trailingslashit( fs_strip_url_protocol( $install->url ) );
16292 $blog_id = $address_to_blog_map[ $address ];
16293
16294 $this->_store_site( true, $blog_id, new FS_Site( $install ) );
16295 }
16296 }
16297 }
16298
16299 // Sync plans.
16300 $this->_sync_plans();
16301 }
16302
16303 // Remove sticky API connectivity message.
16304 self::$_global_admin_notices->remove_sticky( 'api_blocked' );
16305
16306 if ( ! $this->has_paid_plan() ) {
16307 $this->_site = $site;
16308 $this->_store_site(
16309 true,
16310 $is_site_level_sync ?
16311 null :
16312 $this->get_network_install_blog_id()
16313 );
16314 } else {
16315 $context_blog_id = 0;
16316
16317 if ( $is_context_single_site ) {
16318 $context_blog_id = get_current_blog_id();
16319
16320 // Switch back to the main blog in order to properly sync the license.
16321 $this->switch_to_blog( $this->_storage->network_install_blog_id );
16322 }
16323
16324 /**
16325 * Sync licenses. Pass the site's license ID so that the foreign licenses will be fetched if the license
16326 * associated with that ID is not included in the user's licenses collection.
16327 */
16328 $this->_sync_licenses(
16329 $site->license_id,
16330 ( $is_context_single_site ?
16331 $context_blog_id :
16332 null
16333 )
16334 );
16335
16336 if ( $is_context_single_site ) {
16337 $this->switch_to_blog( $context_blog_id );
16338 }
16339
16340 // Check if plan / license changed.
16341 if ( $site->plan_id != $this->_site->plan_id ||
16342 // Check if trial started.
16343 $site->trial_plan_id != $this->_site->trial_plan_id ||
16344 $site->trial_ends != $this->_site->trial_ends ||
16345 // Check if license changed.
16346 $site->license_id != $this->_site->license_id
16347 ) {
16348 if ( $site->is_trial() && ( ! $this->_site->is_trial() || $site->trial_ends != $this->_site->trial_ends ) ) {
16349 // New trial started.
16350 $this->_site = $site;
16351 $plan_change = 'trial_started';
16352
16353 // For trial with subscription use-case.
16354 $new_license = is_null( $site->license_id ) ? null : $this->_get_license_by_id( $site->license_id );
16355
16356 if ( is_object( $new_license ) && $new_license->is_valid() ) {
16357 $this->_site = $site;
16358 $this->_update_site_license( $new_license );
16359 $this->_store_licenses();
16360
16361 $this->_sync_site_subscription( $this->_license );
16362 }
16363 } else if ( $this->_site->is_trial() && ! $site->is_trial() && ! is_numeric( $site->license_id ) ) {
16364 // Was in trial, but now trial expired and no license ID.
16365 // New trial started.
16366 $this->_site = $site;
16367 $plan_change = 'trial_expired';
16368 } else {
16369 $is_free = $this->is_free_plan();
16370
16371 // Make sure license exist and not expired.
16372 $new_license = is_null( $site->license_id ) ?
16373 null :
16374 $this->_get_license_by_id( $site->license_id );
16375
16376 if ( $is_free && is_null( $new_license ) && $this->has_any_license() && $this->_license->is_cancelled ) {
16377 // License cancelled.
16378 $this->_site = $site;
16379 $this->_update_site_license( $new_license );
16380 $this->_store_licenses();
16381
16382 $plan_change = 'cancelled';
16383 } else if ( $is_free && ( ( ! is_object( $new_license ) || $new_license->is_expired() ) ) ) {
16384 // The license is expired, so ignore upgrade method.
16385 $this->_site = $site;
16386 } else {
16387 // License changed.
16388 $this->_site = $site;
16389
16390 /**
16391 * IMPORTANT:
16392 * 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.
16393 *
16394 * @author Vova Feldman (@svovaf)
16395 * @since 2.0.0
16396 */
16397 $this->_update_site_license( $new_license );
16398
16399 if ( ! $is_context_single_site &&
16400 fs_is_network_admin() &&
16401 $this->_is_network_active &&
16402 $new_license->quota > 1 &&
16403 get_blog_count() > 1
16404 ) {
16405 // See if license can activated on all sites.
16406 if ( ! $this->try_activate_license_on_network( $this->_user, $new_license ) ) {
16407 if ( ! fs_request_get_bool( 'auto_install' ) ) {
16408 // Open the license activation dialog box on the account page.
16409 add_action( 'admin_footer', array(
16410 &$this,
16411 '_open_license_activation_dialog_box'
16412 ) );
16413 }
16414 }
16415 }
16416
16417 $this->_store_licenses();
16418
16419 $plan_change = $is_free ?
16420 'upgraded' :
16421 ( is_object( $new_license ) ?
16422 'changed' :
16423 'downgraded' );
16424 }
16425 }
16426
16427 // Store updated site info.
16428 $this->_store_site(
16429 true,
16430 $is_site_level_sync ?
16431 null :
16432 $this->get_network_install_blog_id()
16433 );
16434 } else {
16435 if ( is_object( $this->_license ) && $this->_license->is_expired() ) {
16436 if ( ! $this->has_features_enabled_license() ) {
16437 $this->_deactivate_license();
16438 $plan_change = 'downgraded';
16439 } else {
16440 $plan_change = 'expired';
16441 }
16442 }
16443
16444 if ( is_numeric( $site->license_id ) && is_object( $this->_license ) ) {
16445 $this->_sync_site_subscription( $this->_license );
16446 }
16447 }
16448 }
16449
16450 $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...';
16451
16452 if ( $this->has_paid_plan() ) {
16453 switch ( $plan_change ) {
16454 case 'none':
16455 if ( ! $background && is_admin() ) {
16456 $plan = $this->is_trial() ?
16457 $this->get_trial_plan() :
16458 $this->get_plan();
16459
16460 if ( $plan->is_free() ) {
16461 $this->_admin_notices->add(
16462 sprintf(
16463 $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' ),
16464 '<i><b>' . $plan->title . ( $this->is_trial() ? ' ' . $this->get_text_x_inline( 'Trial', 'trial period', 'trial' ) : '' ) . '</b></i>'
16465 ) . ' ' . sprintf(
16466 '<a href="%s">%s</a>',
16467 $this->contact_url(
16468 'bug',
16469 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' ),
16470 strtoupper( $plan->name )
16471 )
16472 ),
16473 $this->get_text_inline( 'Please contact us here', 'contact-us-here' )
16474 ),
16475 $hmm_text
16476 );
16477 }
16478 }
16479 break;
16480 case 'upgraded':
16481 $this->_admin_notices->add_sticky(
16482 sprintf(
16483 $this->get_text_inline( 'Your plan was successfully upgraded.', 'plan-upgraded-message' ),
16484 '<i>' . $this->get_plugin_name() . '</i>'
16485 ) . $this->get_complete_upgrade_instructions(),
16486 'plan_upgraded',
16487 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
16488 );
16489
16490 $this->_admin_notices->remove_sticky( array(
16491 'trial_started',
16492 'trial_promotion',
16493 'trial_expired',
16494 'activation_complete',
16495 ) );
16496 break;
16497 case 'changed':
16498 $this->_admin_notices->add_sticky(
16499 sprintf(
16500 $this->get_text_inline( 'Your plan was successfully changed to %s.', 'plan-changed-to-x-message' ),
16501 $this->get_plan_title()
16502 ),
16503 'plan_changed'
16504 );
16505
16506 $this->_admin_notices->remove_sticky( array(
16507 'trial_started',
16508 'trial_promotion',
16509 'trial_expired',
16510 'activation_complete',
16511 ) );
16512 break;
16513 case 'downgraded':
16514 $this->_admin_notices->add_sticky(
16515 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 ),
16516 'license_expired',
16517 $hmm_text
16518 );
16519 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
16520 break;
16521 case 'cancelled':
16522 $this->_admin_notices->add(
16523 $this->get_text_inline( 'Your license has been cancelled. If you think it\'s a mistake, please contact support.', 'license-cancelled' ) . ' ' .
16524 sprintf(
16525 '<a href="%s">%s</a>',
16526 $this->contact_url( 'bug' ),
16527 $this->get_text_inline( 'Please contact us here', 'contact-us-here' )
16528 ),
16529 $hmm_text,
16530 'error'
16531 );
16532 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
16533 break;
16534 case 'expired':
16535 $this->_admin_notices->add_sticky(
16536 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 ),
16537 'license_expired',
16538 $hmm_text
16539 );
16540 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
16541 break;
16542 case 'trial_started':
16543 $this->_admin_notices->add_sticky(
16544 sprintf(
16545 $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ),
16546 '<i>' . $this->get_plugin_name() . '</i>'
16547 ) . $this->get_complete_upgrade_instructions( $this->get_trial_plan()->title ),
16548 'trial_started',
16549 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
16550 );
16551
16552 $this->_admin_notices->remove_sticky( array(
16553 'trial_promotion',
16554 ) );
16555 break;
16556 case 'trial_expired':
16557 $this->_admin_notices->add_sticky(
16558 $this->get_text_inline( 'Your trial has expired. You can still continue using all our free features.', 'trial-expired-message' ),
16559 'trial_expired',
16560 $hmm_text
16561 );
16562 $this->_admin_notices->remove_sticky( array(
16563 'trial_started',
16564 'trial_promotion',
16565 'plan_upgraded',
16566 ) );
16567 break;
16568 }
16569 }
16570
16571 if ( 'none' !== $plan_change ) {
16572 $this->do_action( 'after_license_change', $plan_change, $this->get_plan() );
16573 }
16574 }
16575
16576 /**
16577 * Include the required JS at the footer of the admin to trigger the license activation dialog box.
16578 *
16579 * @author Vova Feldman (@svovaf)
16580 * @since 2.0.0
16581 */
16582 public function _open_license_activation_dialog_box() {
16583 $vars = array( 'license_id' => $this->_site->license_id );
16584 fs_require_once_template( 'js/open-license-activation.php', $vars );
16585 }
16586
16587 /**
16588 * @author Vova Feldman (@svovaf)
16589 * @since 1.0.5
16590 *
16591 * @param bool $background
16592 */
16593 protected function _activate_license( $background = false ) {
16594 $this->_logger->entrance();
16595
16596 $license_id = fs_request_get( 'license_id' );
16597
16598 if ( is_object( $this->_site ) &&
16599 FS_Plugin_License::is_valid_id( $license_id ) &&
16600 $license_id == $this->_site->license_id
16601 ) {
16602 // License is already activated.
16603 return;
16604 }
16605
16606 $premium_license = FS_Plugin_License::is_valid_id( $license_id ) ?
16607 $this->_get_license_by_id( $license_id ) :
16608 $this->_get_available_premium_license();
16609
16610 if ( ! is_object( $premium_license ) ) {
16611 return;
16612 }
16613
16614 if ( ! is_object( $this->_site ) ) {
16615 // Not yet opted-in.
16616 $user = $this->get_current_or_network_user();
16617 if ( ! is_object( $user ) ) {
16618 $user = self::_get_user_by_id( $premium_license->user_id );
16619 }
16620
16621 if ( is_object( $user ) ) {
16622 $this->install_with_user( $user, $premium_license->secret_key, false, false, false );
16623 } else {
16624 $this->opt_in(
16625 false,
16626 false,
16627 false,
16628 $premium_license->secret_key
16629 );
16630
16631 return;
16632 }
16633 }
16634
16635
16636 /**
16637 * If the premium license is already associated with the install, just
16638 * update the license reference (activation is not required).
16639 *
16640 * @since 1.1.9
16641 */
16642 if ( $premium_license->id == $this->_site->license_id ) {
16643 // License is already activated.
16644 $this->_update_site_license( $premium_license );
16645 $this->_store_account();
16646
16647 return;
16648 }
16649
16650 if ( $this->_site->user_id != $premium_license->user_id ) {
16651 $api_request_params = array( 'license_key' => $premium_license->secret_key );
16652 } else {
16653 $api_request_params = array();
16654 }
16655
16656 $api = $this->get_api_site_scope();
16657 $license = $api->call( "/licenses/{$premium_license->id}.json", 'put', $api_request_params );
16658
16659 if ( ! $this->is_api_result_entity( $license ) ) {
16660 if ( ! $background ) {
16661 $this->_admin_notices->add( sprintf(
16662 '%s %s',
16663 $this->get_text_inline( 'It looks like the license could not be activated.', 'license-activation-failed-message' ),
16664 ( is_object( $license ) && isset( $license->error ) ?
16665 $license->error->message :
16666 sprintf( '%s<br><code>%s</code>',
16667 $this->get_text_inline( 'Error received from the server:', 'server-error-message' ),
16668 var_export( $license, true )
16669 )
16670 )
16671 ),
16672 $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...',
16673 'error'
16674 );
16675 }
16676
16677 return;
16678 }
16679
16680 $premium_license = new FS_Plugin_License( $license );
16681
16682 // Updated site plan.
16683 $site = $this->get_api_site_scope()->get( '/', true );
16684 if ( $this->is_api_result_entity( $site ) ) {
16685 $this->_site = new FS_Site( $site );
16686 }
16687 $this->_update_site_license( $premium_license );
16688
16689 $this->_store_account();
16690
16691 if ( ! $background ) {
16692 $this->_admin_notices->add_sticky(
16693 $this->get_text_inline( 'Your license was successfully activated.', 'license-activated-message' ) .
16694 $this->get_complete_upgrade_instructions(),
16695 'license_activated',
16696 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
16697 );
16698 }
16699
16700 $this->_admin_notices->remove_sticky( array(
16701 'trial_promotion',
16702 'license_expired',
16703 ) );
16704 }
16705
16706 /**
16707 * @author Vova Feldman (@svovaf)
16708 * @since 1.0.5
16709 *
16710 * @param bool $show_notice
16711 */
16712 protected function _deactivate_license( $show_notice = true ) {
16713 $this->_logger->entrance();
16714
16715 $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...';
16716
16717 if ( ! FS_Plugin_License::is_valid_id( $this->_site->license_id ) ) {
16718 $this->_admin_notices->add(
16719 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() ),
16720 $hmm_text
16721 );
16722
16723 return;
16724 }
16725
16726 $api = $this->get_api_site_scope();
16727 $license = $api->call( "/licenses/{$this->_site->license_id}.json", 'delete' );
16728
16729 if ( isset( $license->error ) ) {
16730 $this->_admin_notices->add(
16731 $this->get_text_inline( 'It looks like the license deactivation failed.', 'license-deactivation-failed-message' ) . '<br> ' .
16732 $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . ' ' . var_export( $license->error, true ),
16733 $hmm_text,
16734 'error'
16735 );
16736
16737 return;
16738 }
16739
16740 // Update license cache.
16741 if ( is_array( $this->_licenses ) ) {
16742 for ( $i = 0, $len = count( $this->_licenses ); $i < $len; $i ++ ) {
16743 if ( $license->id == $this->_licenses[ $i ]->id ) {
16744 $this->_licenses[ $i ] = new FS_Plugin_License( $license );
16745 }
16746 }
16747 }
16748
16749 // Updated site plan to default.
16750 $this->_sync_plans();
16751 $this->_site->plan_id = $this->_plans[0]->id;
16752 // Unlink license from site.
16753 $this->_update_site_license( null );
16754
16755 $this->_store_account();
16756
16757 if ( $show_notice ) {
16758 $this->_admin_notices->add(
16759 sprintf( $this->get_text_inline( 'Your license was successfully deactivated, you are back to the %s plan.', 'license-deactivation-message' ), $this->get_plan_title() ),
16760 $this->get_text_inline( 'O.K', 'ok' )
16761 );
16762 }
16763
16764 $this->_admin_notices->remove_sticky( array(
16765 'plan_upgraded',
16766 'license_activated',
16767 ) );
16768 }
16769
16770 /**
16771 * Site plan downgrade.
16772 *
16773 * @author Vova Feldman (@svovaf)
16774 * @since 1.0.4
16775 *
16776 * @uses FS_Api
16777 */
16778 private function _downgrade_site() {
16779 $this->_logger->entrance();
16780
16781 $api = $this->get_api_site_scope();
16782 $site = $api->call( 'downgrade.json', 'put' );
16783
16784 $plan_downgraded = false;
16785 $plan = false;
16786 if ( $this->is_api_result_entity( $site ) ) {
16787 $prev_plan_id = $this->_site->plan_id;
16788
16789 // Update new site plan id.
16790 $this->_site->plan_id = $site->plan_id;
16791
16792 $plan = $this->get_plan();
16793 $subscription = $this->_sync_site_subscription( $this->_license );
16794
16795 // Plan downgraded if plan was changed or subscription was cancelled.
16796 $plan_downgraded = ( $plan instanceof FS_Plugin_Plan && $prev_plan_id != $plan->id ) ||
16797 ( is_object( $subscription ) && ! isset( $subscription->error ) && ! $subscription->is_active() );
16798 } else {
16799 // handle different error cases.
16800
16801 }
16802
16803 if ( $plan_downgraded ) {
16804 // Remove previous sticky message about upgrade (if exist).
16805 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
16806
16807 $this->_admin_notices->add(
16808 sprintf( $this->get_text_inline( 'Your plan was successfully downgraded. Your %s plan license will expire in %s.', 'plan-x-downgraded-message' ),
16809 $plan->title,
16810 human_time_diff( time(), strtotime( $this->_license->expiration ) )
16811 )
16812 );
16813
16814 // Store site updates.
16815 $this->_store_site();
16816 } else {
16817 $this->_admin_notices->add(
16818 $this->get_text_inline( 'Seems like we are having some temporary issue with your plan downgrade. Please try again in few minutes.', 'plan-downgraded-failure-message' ),
16819 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
16820 'error'
16821 );
16822 }
16823 }
16824
16825 /**
16826 * @author Vova Feldman (@svovaf)
16827 * @since 1.1.8.1
16828 *
16829 * @param bool|string $plan_name
16830 *
16831 * @return bool If trial was successfully started.
16832 */
16833 function start_trial( $plan_name = false ) {
16834 $this->_logger->entrance();
16835
16836 // Alias.
16837 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
16838
16839 if ( $this->is_trial() ) {
16840 // Already in trial mode.
16841 $this->_admin_notices->add(
16842 sprintf( $this->get_text_inline( 'You are already running the %s in a trial mode.', 'in-trial-mode' ), $this->_module_type ),
16843 $oops_text,
16844 'error'
16845 );
16846
16847 return false;
16848 }
16849
16850 if ( $this->_site->is_trial_utilized() ) {
16851 // Trial was already utilized.
16852 $this->_admin_notices->add(
16853 $this->get_text_inline( 'You already utilized a trial before.', 'trial-utilized' ),
16854 $oops_text,
16855 'error'
16856 );
16857
16858 return false;
16859 }
16860
16861 if ( false !== $plan_name ) {
16862 $plan = $this->get_plan_by_name( $plan_name );
16863
16864 if ( false === $plan ) {
16865 // Plan doesn't exist.
16866 $this->_admin_notices->add(
16867 sprintf( $this->get_text_inline( 'Plan %s do not exist, therefore, can\'t start a trial.', 'trial-plan-x-not-exist' ), $plan_name ),
16868 $oops_text,
16869 'error'
16870 );
16871
16872 return false;
16873 }
16874
16875 if ( ! $plan->has_trial() ) {
16876 // Plan doesn't exist.
16877 $this->_admin_notices->add(
16878 sprintf( $this->get_text_inline( 'Plan %s does not support a trial period.', 'plan-x-no-trial' ), $plan_name ),
16879 $oops_text,
16880 'error'
16881 );
16882
16883 return false;
16884 }
16885 } else {
16886 if ( ! $this->has_trial_plan() ) {
16887 // None of the plans have a trial.
16888 $this->_admin_notices->add(
16889 sprintf( $this->get_text_inline( 'None of the %s\'s plans supports a trial period.', 'no-trials' ), $this->_module_type ),
16890 $oops_text,
16891 'error'
16892 );
16893
16894 return false;
16895 }
16896
16897 $plans_with_trial = FS_Plan_Manager::instance()->get_trial_plans( $this->_plans );
16898
16899 $plan = $plans_with_trial[0];
16900 }
16901
16902 $api = $this->get_api_site_scope();
16903 $plan = $api->call( "plans/{$plan->id}/trials.json", 'post' );
16904
16905 if ( ! $this->is_api_result_entity( $plan ) ) {
16906 // Some API error while trying to start the trial.
16907 $this->_admin_notices->add(
16908 sprintf( $this->get_text_inline( 'Unexpected API error. Please contact the %s\'s author with the following error.', 'unexpected-api-error' ), $this->_module_type )
16909 . ' ' . var_export( $plan, true ),
16910 $oops_text,
16911 'error'
16912 );
16913
16914 return false;
16915 }
16916
16917 // Sync license.
16918 $this->_sync_license();
16919
16920 return $this->is_trial();
16921 }
16922
16923 /**
16924 * Cancel site trial.
16925 *
16926 * @author Vova Feldman (@svovaf)
16927 * @since 1.0.9
16928 *
16929 * @uses FS_Api
16930 */
16931 private function _cancel_trial() {
16932 $this->_logger->entrance();
16933
16934 // Alias.
16935 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
16936
16937 if ( ! $this->is_trial() ) {
16938 $this->_admin_notices->add(
16939 $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' ),
16940 $oops_text,
16941 'error'
16942 );
16943
16944 return;
16945 }
16946
16947 $trial_plan = $this->get_trial_plan();
16948
16949 $api = $this->get_api_site_scope();
16950 $site = $api->call( 'trials.json', 'delete' );
16951
16952 $trial_cancelled = false;
16953
16954 if ( $this->is_api_result_entity( $site ) ) {
16955 $prev_trial_ends = $this->_site->trial_ends;
16956
16957 if ( $this->is_paid_trial() ) {
16958 $this->_license->expiration = $site->trial_ends;
16959 $this->_license->is_cancelled = true;
16960 $this->_update_site_license( $this->_license );
16961 $this->_store_licenses();
16962
16963 // Clear subscription reference.
16964 $this->_sync_site_subscription( null );
16965 }
16966
16967 // Update site info.
16968 $this->_site = new FS_Site( $site );
16969
16970 $trial_cancelled = ( $prev_trial_ends != $site->trial_ends );
16971 } else {
16972 // @todo handle different error cases.
16973 }
16974
16975 if ( $trial_cancelled ) {
16976 // Remove previous sticky messages about upgrade or trial (if exist).
16977 $this->_admin_notices->remove_sticky( array(
16978 'trial_started',
16979 'trial_promotion',
16980 'plan_upgraded',
16981 ) );
16982
16983 // Store site updates.
16984 $this->_store_site();
16985
16986 if ( ! $this->is_addon() ||
16987 ! $this->deactivate_premium_only_addon_without_license( true )
16988 ) {
16989 $this->_admin_notices->add(
16990 sprintf( $this->get_text_inline( 'Your %s free trial was successfully cancelled.', 'trial-cancel-message' ), $trial_plan->title )
16991 );
16992 }
16993 } else {
16994 $this->_admin_notices->add(
16995 $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' ),
16996 $oops_text,
16997 'error'
16998 );
16999 }
17000 }
17001
17002 /**
17003 * @author Vova Feldman (@svovaf)
17004 * @since 1.0.6
17005 *
17006 * @param bool|number $plugin_id
17007 *
17008 * @return bool
17009 */
17010 private function _is_addon_id( $plugin_id ) {
17011 return is_numeric( $plugin_id ) && ( $this->get_id() != $plugin_id );
17012 }
17013
17014 /**
17015 * Check if user eligible to download premium version updates.
17016 *
17017 * @author Vova Feldman (@svovaf)
17018 * @since 1.0.6
17019 *
17020 * @return bool
17021 */
17022 private function _can_download_premium() {
17023 return $this->has_active_valid_license() ||
17024 ( $this->is_trial() && ! $this->get_trial_plan()->is_free() );
17025 }
17026
17027 /**
17028 *
17029 * @author Vova Feldman (@svovaf)
17030 * @since 1.0.6
17031 *
17032 * @param bool|number $addon_id
17033 * @param string $type "json" or "zip"
17034 *
17035 * @return string
17036 */
17037 private function _get_latest_version_endpoint( $addon_id = false, $type = 'json' ) {
17038
17039 $is_addon = $this->_is_addon_id( $addon_id );
17040
17041 $is_premium = null;
17042 if ( ! $is_addon ) {
17043 $is_premium = $this->_can_download_premium();
17044 } else if ( $this->is_addon_activated( $addon_id ) ) {
17045 $is_premium = self::get_instance_by_id( $addon_id )->_can_download_premium();
17046 }
17047
17048 // If add-on, then append add-on ID.
17049 $endpoint = ( $is_addon ? "/addons/$addon_id" : '' ) .
17050 '/updates/latest.' . $type;
17051
17052 // If add-on and not yet activated, try to fetch based on server licensing.
17053 if ( is_bool( $is_premium ) ) {
17054 $endpoint = add_query_arg( 'is_premium', json_encode( $is_premium ), $endpoint );
17055 }
17056
17057 if ( $this->has_secret_key() ) {
17058 $endpoint = add_query_arg( 'type', 'all', $endpoint );
17059 }
17060
17061 return $endpoint;
17062 }
17063
17064 /**
17065 * @author Vova Feldman (@svovaf)
17066 * @since 1.0.4
17067 *
17068 * @param bool|number $addon_id
17069 * @param bool $flush Since 1.1.7.3
17070 * @param int $expiration Since 1.2.2.7
17071 *
17072 * @return object|false Plugin latest tag info.
17073 */
17074 function _fetch_latest_version(
17075 $addon_id = false,
17076 $flush = true,
17077 $expiration = WP_FS__TIME_24_HOURS_IN_SEC
17078 ) {
17079 $this->_logger->entrance();
17080
17081 $switch_to_blog_id = null;
17082
17083 /**
17084 * @since 1.1.7.3 Check for plugin updates from Freemius only if opted-in.
17085 * @since 1.1.7.4 Also check updates for add-ons.
17086 */
17087 if ( ! $this->is_registered() &&
17088 ! $this->_is_addon_id( $addon_id )
17089 ) {
17090 if ( ! is_multisite() ) {
17091 return false;
17092 }
17093
17094 $installs_map = $this->get_blog_install_map();
17095
17096 foreach ( $installs_map as $blog_id => $install ) {
17097 /**
17098 * @var FS_Site $install
17099 */
17100 if ( $install->is_trial() ) {
17101 $switch_to_blog_id = $blog_id;
17102 break;
17103 }
17104
17105 if ( FS_Plugin_License::is_valid_id( $install->license_id ) ) {
17106 $license = $this->get_license_by_id( $install->license_id );
17107
17108 if ( is_object( $license ) && $license->is_features_enabled() ) {
17109 $switch_to_blog_id = $blog_id;
17110 break;
17111 }
17112 }
17113 }
17114
17115 if ( is_null( $switch_to_blog_id ) ) {
17116 return false;
17117 }
17118 }
17119
17120 $current_blog_id = is_numeric( $switch_to_blog_id ) ?
17121 get_current_blog_id() :
17122 0;
17123
17124 if ( is_numeric( $switch_to_blog_id ) ) {
17125 $this->switch_to_blog( $switch_to_blog_id );
17126 }
17127
17128 $tag = $this->get_api_site_or_plugin_scope()->get(
17129 $this->_get_latest_version_endpoint( $addon_id, 'json' ),
17130 $flush,
17131 $expiration
17132 );
17133
17134 if ( is_numeric( $switch_to_blog_id ) ) {
17135 $this->switch_to_blog( $current_blog_id );
17136 }
17137
17138 $latest_version = ( is_object( $tag ) && isset( $tag->version ) ) ? $tag->version : 'couldn\'t get';
17139
17140 $this->_logger->departure( 'Latest version ' . $latest_version );
17141
17142 return ( is_object( $tag ) && isset( $tag->version ) ) ? $tag : false;
17143 }
17144
17145 #----------------------------------------------------------------------------------
17146 #region Download Plugin
17147 #----------------------------------------------------------------------------------
17148
17149 /**
17150 * Download latest plugin version, based on plan.
17151 *
17152 * Not like _download_latest(), this will redirect the page
17153 * to secure download url to prevent dual download (from FS to WP server,
17154 * and then from WP server to the client / browser).
17155 *
17156 * @author Vova Feldman (@svovaf)
17157 * @since 1.0.9
17158 *
17159 * @param bool|number $plugin_id
17160 *
17161 * @uses FS_Api
17162 * @uses wp_redirect()
17163 */
17164 private function download_latest_directly( $plugin_id = false ) {
17165 $this->_logger->entrance();
17166
17167 wp_redirect( $this->get_latest_download_api_url( $plugin_id ) );
17168 }
17169
17170 /**
17171 * Get latest plugin FS API download URL.
17172 *
17173 * @author Vova Feldman (@svovaf)
17174 * @since 1.0.9
17175 *
17176 * @param bool|number $plugin_id
17177 *
17178 * @return string
17179 */
17180 private function get_latest_download_api_url( $plugin_id = false ) {
17181 $this->_logger->entrance();
17182
17183 return $this->get_api_site_scope()->get_signed_url(
17184 $this->_get_latest_version_endpoint( $plugin_id, 'zip' )
17185 );
17186 }
17187
17188 /**
17189 * Get payment invoice URL.
17190 *
17191 * @author Vova Feldman (@svovaf)
17192 * @since 1.2.0
17193 *
17194 * @param bool|number $payment_id
17195 *
17196 * @return string
17197 */
17198 function _get_invoice_api_url( $payment_id = false ) {
17199 $this->_logger->entrance();
17200
17201 return $this->get_api_user_scope()->get_signed_url(
17202 "/payments/{$payment_id}/invoice.pdf"
17203 );
17204 }
17205
17206 /**
17207 * Get latest plugin download link.
17208 *
17209 * @author Vova Feldman (@svovaf)
17210 * @since 1.0.9
17211 *
17212 * @param string $label
17213 * @param bool|number $plugin_id
17214 *
17215 * @return string
17216 */
17217 private function get_latest_download_link( $label, $plugin_id = false ) {
17218 return sprintf(
17219 '<a target="_blank" href="%s">%s</a>',
17220 $this->_get_latest_download_local_url( $plugin_id ),
17221 $label
17222 );
17223 }
17224
17225 /**
17226 * Get latest plugin download local URL.
17227 *
17228 * @author Vova Feldman (@svovaf)
17229 * @since 1.0.9
17230 *
17231 * @param bool|number $plugin_id
17232 *
17233 * @return string
17234 */
17235 function _get_latest_download_local_url( $plugin_id = false ) {
17236 // Add timestamp to protect from caching.
17237 $params = array( 'ts' => WP_FS__SCRIPT_START_TIME );
17238
17239 if ( ! empty( $plugin_id ) ) {
17240 $params['plugin_id'] = $plugin_id;
17241 }
17242
17243 return $this->get_account_url( 'download_latest', $params );
17244 }
17245
17246 #endregion Download Plugin ------------------------------------------------------------------
17247
17248 /**
17249 * @author Vova Feldman (@svovaf)
17250 * @since 1.0.4
17251 *
17252 * @uses FS_Api
17253 *
17254 * @param bool $background Hints the method if it's a background updates check. If false, it means that
17255 * was initiated by the admin.
17256 * @param bool|number $plugin_id
17257 * @param bool $flush Since 1.1.7.3
17258 * @param int $expiration Since 1.2.2.7
17259 */
17260 private function check_updates(
17261 $background = false,
17262 $plugin_id = false,
17263 $flush = true,
17264 $expiration = WP_FS__TIME_24_HOURS_IN_SEC
17265 ) {
17266 $this->_logger->entrance();
17267
17268 // Check if there's a newer version for download.
17269 $new_version = $this->_fetch_newer_version( $plugin_id, $flush, $expiration );
17270
17271 $update = null;
17272 if ( is_object( $new_version ) ) {
17273 $update = new FS_Plugin_Tag( $new_version );
17274
17275 if ( ! $background ) {
17276 $this->_admin_notices->add(
17277 sprintf(
17278 /* translators: %s: Numeric version number (e.g. '2.1.9' */
17279 $this->get_text_inline( 'Version %s was released.', 'version-x-released' ) . ' ' . $this->get_text_inline( 'Please download %s.', 'please-download-x' ),
17280 $update->version,
17281 sprintf(
17282 '<a href="%s" target="_blank">%s</a>',
17283 $this->get_account_url( 'download_latest' ),
17284 sprintf(
17285 /* translators: %s: plan name (e.g. latest "Professional" version) */
17286 $this->get_text_inline( 'the latest %s version here', 'latest-x-version' ),
17287 $this->get_plan_title()
17288 )
17289 )
17290 ),
17291 $this->get_text_inline( 'New', 'new' ) . '!'
17292 );
17293 }
17294 } else if ( false === $new_version && ! $background ) {
17295 $this->_admin_notices->add(
17296 $this->get_text_inline( 'Seems like you got the latest release.', 'you-have-latest' ),
17297 $this->get_text_inline( 'You are all good!', 'you-are-good' )
17298 );
17299 }
17300
17301 $this->_store_update( $update, true, $plugin_id );
17302 }
17303
17304 /**
17305 * @author Vova Feldman (@svovaf)
17306 * @since 1.0.4
17307 *
17308 * @param bool $flush Since 1.1.7.3 add 24 hour cache by default.
17309 *
17310 * @return FS_Plugin[]
17311 *
17312 * @uses FS_Api
17313 */
17314 private function sync_addons( $flush = false ) {
17315 $this->_logger->entrance();
17316
17317 $api = $this->get_api_site_or_plugin_scope();
17318
17319 /**
17320 * @since 1.2.1
17321 *
17322 * If there's a cached version of the add-ons and not asking
17323 * for a flush, just use the currently stored add-ons.
17324 */
17325 if ( ! $flush && $api->is_cached( '/addons.json?enriched=true' ) ) {
17326 $addons = self::get_all_addons();
17327
17328 return $addons[ $this->_plugin->id ];
17329 }
17330
17331 $result = $api->get( '/addons.json?enriched=true', $flush );
17332
17333 $addons = array();
17334 if ( $this->is_api_result_object( $result, 'plugins' ) &&
17335 is_array( $result->plugins )
17336 ) {
17337 for ( $i = 0, $len = count( $result->plugins ); $i < $len; $i ++ ) {
17338 $addons[ $i ] = new FS_Plugin( $result->plugins[ $i ] );
17339 }
17340
17341 $this->_store_addons( $addons, true );
17342 }
17343
17344 return $addons;
17345 }
17346
17347 /**
17348 * Handle user email update.
17349 *
17350 * @author Vova Feldman (@svovaf)
17351 * @since 1.0.3
17352 * @uses FS_Api
17353 *
17354 * @param string $new_email
17355 *
17356 * @return object
17357 */
17358 private function update_email( $new_email ) {
17359 $this->_logger->entrance();
17360
17361
17362 $api = $this->get_api_user_scope();
17363 $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,email,is_verified", 'put', array(
17364 'email' => $new_email,
17365 'after_email_confirm_url' => $this->_get_admin_page_url(
17366 'account',
17367 array( 'fs_action' => 'sync_user' )
17368 ),
17369 ) );
17370
17371 if ( ! isset( $user->error ) ) {
17372 $this->_user->email = $user->email;
17373 $this->_user->is_verified = $user->is_verified;
17374 $this->_store_user();
17375 } else {
17376 // handle different error cases.
17377
17378 }
17379
17380 return $user;
17381 }
17382
17383 #----------------------------------------------------------------------------------
17384 #region API Error Handling
17385 #----------------------------------------------------------------------------------
17386
17387 /**
17388 * @author Vova Feldman (@svovaf)
17389 * @since 1.1.1
17390 *
17391 * @param mixed $result
17392 *
17393 * @return bool Is API result contains an error.
17394 */
17395 private function is_api_error( $result ) {
17396 return FS_Api::is_api_error( $result );
17397 }
17398
17399 /**
17400 * Checks if given API result is a non-empty and not an error object.
17401 *
17402 * @author Vova Feldman (@svovaf)
17403 * @since 1.2.1.5
17404 *
17405 * @param mixed $result
17406 * @param string|null $required_property Optional property we want to verify that is set.
17407 *
17408 * @return bool
17409 */
17410 function is_api_result_object( $result, $required_property = null ) {
17411 return FS_Api::is_api_result_object( $result, $required_property );
17412 }
17413
17414 /**
17415 * Checks if given API result is a non-empty entity object with non-empty ID.
17416 *
17417 * @author Vova Feldman (@svovaf)
17418 * @since 1.2.1.5
17419 *
17420 * @param mixed $result
17421 *
17422 * @return bool
17423 */
17424 private function is_api_result_entity( $result ) {
17425 return FS_Api::is_api_result_entity( $result );
17426 }
17427
17428 #endregion
17429
17430 /**
17431 * Make sure a given argument is an array of a specific type.
17432 *
17433 * @author Vova Feldman (@svovaf)
17434 * @since 1.2.1.5
17435 *
17436 * @param mixed $array
17437 * @param string $class
17438 *
17439 * @return bool
17440 */
17441 private function is_array_instanceof( $array, $class ) {
17442 return ( is_array( $array ) && ( empty( $array ) || $array[0] instanceof $class ) );
17443 }
17444
17445 /**
17446 * Start install ownership change.
17447 *
17448 * @author Vova Feldman (@svovaf)
17449 * @since 1.1.1
17450 * @uses FS_Api
17451 *
17452 * @param string $candidate_email
17453 *
17454 * @return bool Is ownership change successfully initiated.
17455 */
17456 private function init_change_owner( $candidate_email ) {
17457 $this->_logger->entrance();
17458
17459 $api = $this->get_api_site_scope();
17460 $result = $api->call( "/users/{$this->_user->id}.json", 'put', array(
17461 'email' => $candidate_email,
17462 'after_confirm_url' => $this->_get_admin_page_url(
17463 'account',
17464 array( 'fs_action' => 'change_owner' )
17465 ),
17466 ) );
17467
17468 return ! $this->is_api_error( $result );
17469 }
17470
17471 /**
17472 * Handle install ownership change.
17473 *
17474 * @author Vova Feldman (@svovaf)
17475 * @since 1.1.1
17476 * @uses FS_Api
17477 *
17478 * @return bool Was ownership change successfully complete.
17479 */
17480 private function complete_change_owner() {
17481 $this->_logger->entrance();
17482
17483 $site_result = $this->get_api_site_scope( true )->get();
17484 $site = new FS_Site( $site_result );
17485 $this->_site = $site;
17486
17487 $user = new FS_User();
17488 $user->id = fs_request_get( 'user_id' );
17489
17490 // Validate install's user and given user.
17491 if ( $user->id != $this->_site->user_id ) {
17492 return false;
17493 }
17494
17495 $user->public_key = fs_request_get( 'user_public_key' );
17496 $user->secret_key = fs_request_get( 'user_secret_key' );
17497
17498 // Fetch new user information.
17499 $this->_user = $user;
17500 $user_result = $this->get_api_user_scope( true )->get();
17501 $user = new FS_User( $user_result );
17502 $this->_user = $user;
17503
17504 $this->_set_account( $user, $site );
17505
17506 return true;
17507 }
17508
17509 /**
17510 * Handle user name update.
17511 *
17512 * @author Vova Feldman (@svovaf)
17513 * @since 1.0.9
17514 * @uses FS_Api
17515 *
17516 * @return object
17517 */
17518 private function update_user_name() {
17519 $this->_logger->entrance();
17520 $name = fs_request_get( 'fs_user_name_' . $this->get_unique_affix(), '' );
17521
17522 $api = $this->get_api_user_scope();
17523 $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,first,last", 'put', array(
17524 'name' => $name,
17525 ) );
17526
17527 if ( ! isset( $user->error ) ) {
17528 $this->_user->first = $user->first;
17529 $this->_user->last = $user->last;
17530 $this->_store_user();
17531 } else {
17532 // handle different error cases.
17533
17534 }
17535
17536 return $user;
17537 }
17538
17539 /**
17540 * Verify user email.
17541 *
17542 * @author Vova Feldman (@svovaf)
17543 * @since 1.0.3
17544 * @uses FS_Api
17545 */
17546 private function verify_email() {
17547 $this->_handle_account_user_sync();
17548
17549 if ( $this->_user->is_verified() ) {
17550 return;
17551 }
17552
17553 $api = $this->get_api_site_scope();
17554 $result = $api->call( "/users/{$this->_user->id}/verify.json", 'put', array(
17555 'after_email_confirm_url' => $this->_get_admin_page_url(
17556 'account',
17557 array( 'fs_action' => 'sync_user' )
17558 )
17559 ) );
17560
17561 if ( ! isset( $result->error ) ) {
17562 $this->_admin_notices->add( sprintf(
17563 $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' ),
17564 sprintf( '<a href="mailto:%1s">%2s</a>', esc_url( $this->_user->email ), $this->_user->email )
17565 ) );
17566 } else {
17567 // handle different error cases.
17568
17569 }
17570 }
17571
17572 /**
17573 * @author Vova Feldman (@svovaf)
17574 * @since 1.1.2
17575 *
17576 * @param array $params
17577 * @param bool|null $network
17578 *
17579 * @return string
17580 */
17581 function get_activation_url( $params = array(), $network = null ) {
17582 if ( $this->is_addon() && $this->has_free_plan() ) {
17583 /**
17584 * @author Vova Feldman (@svovaf)
17585 * @since 1.2.1.7 Add-on's activation is the parent's module activation.
17586 */
17587 return $this->get_parent_instance()->get_activation_url( $params );
17588 }
17589
17590 return $this->apply_filters( 'connect_url', $this->_get_admin_page_url( '', $params, $network ) );
17591 }
17592
17593 /**
17594 * @author Vova Feldman (@svovaf)
17595 * @since 1.2.1.5
17596 *
17597 * @param array $params
17598 *
17599 * @return string
17600 */
17601 function get_reconnect_url( $params = array() ) {
17602 $params['fs_action'] = 'reset_anonymous_mode';
17603 $params['fs_unique_affix'] = $this->get_unique_affix();
17604
17605 return $this->get_activation_url( $params );
17606 }
17607
17608 /**
17609 * Get the URL of the page that should be loaded after the user connect
17610 * or skip in the opt-in screen.
17611 *
17612 * @author Vova Feldman (@svovaf)
17613 * @since 1.1.3
17614 *
17615 * @param string $filter Filter name.
17616 * @param array $params Since 1.2.2.7
17617 * @param bool|null $network
17618 *
17619 * @return string
17620 */
17621 function get_after_activation_url( $filter, $params = array(), $network = null ) {
17622 if ( $this->is_free_wp_org_theme() &&
17623 fs_request_has( 'pending_activation' )
17624 ) {
17625 $first_time_path = '';
17626 } else {
17627 $first_time_path = $this->_menu->get_first_time_path();
17628 }
17629
17630 if ( $this->_is_network_active &&
17631 fs_is_network_admin() &&
17632 ! $this->_menu->has_network_menu() &&
17633 $this->is_network_registered()
17634 ) {
17635 $target_url = $this->get_account_url();
17636 } else {
17637 // Default plugin's page.
17638 $target_url = $this->_get_admin_page_url( '', array(), $network );
17639 }
17640
17641 return add_query_arg( $params, $this->apply_filters(
17642 $filter,
17643 empty( $first_time_path ) ?
17644 $target_url :
17645 $first_time_path
17646 ) );
17647 }
17648
17649 /**
17650 * Handle account page updates / edits / actions.
17651 *
17652 * @author Vova Feldman (@svovaf)
17653 * @since 1.0.2
17654 *
17655 */
17656 private function _handle_account_edits() {
17657 if ( ! $this->is_user_admin() ) {
17658 return;
17659 }
17660
17661 $action = fs_get_action();
17662
17663 if ( empty( $action ) ) {
17664 return;
17665 }
17666
17667 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
17668 $install_id = fs_request_get( 'install_id', '' );
17669
17670 // Alias.
17671 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
17672
17673 $is_network_action = $this->is_network_level_action();
17674 $blog_id = $this->is_network_level_site_specific_action();
17675
17676 if ( is_numeric( $blog_id ) ) {
17677 $this->switch_to_blog( $blog_id );
17678 } else {
17679 $blog_id = '';
17680 }
17681
17682 switch ( $action ) {
17683 case 'opt_in':
17684 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
17685
17686 if ( $plugin_id == $this->get_id() ) {
17687 if ( $is_network_action && ! empty( $blog_id ) ) {
17688 if ( ! $this->is_registered() ) {
17689 $this->install_with_user(
17690 $this->get_network_user(),
17691 false,
17692 false,
17693 false,
17694 false
17695 );
17696
17697 $this->_admin_notices->add(
17698 $this->get_text_inline( 'Site successfully opted in.', 'successful-opt-in' ),
17699 $this->get_text_inline( 'Awesome', 'awesome' )
17700 );
17701 }
17702 }
17703 }
17704 break;
17705
17706 case 'toggle_tracking':
17707 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
17708
17709 if ( $plugin_id == $this->get_id() ) {
17710 if ( $is_network_action && ! empty( $blog_id ) ) {
17711 if ( $this->is_registered() ) {
17712 if ( $this->is_tracking_prohibited() ) {
17713 if ( $this->allow_site_tracking() ) {
17714 $this->_admin_notices->add(
17715 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 ),
17716 $this->get_text_inline( 'Thank you!', 'thank-you' )
17717 );
17718 }
17719 } else {
17720 if ( $this->stop_site_tracking() ) {
17721 $this->_admin_notices->add(
17722 sprintf(
17723 $this->get_text_inline( 'We will no longer be sending any usage data of %s on %s to %s.', 'opted-out-successfully' ),
17724 $this->get_plugin_title(),
17725 fs_strip_url_protocol( get_site_url( $blog_id ) ),
17726 sprintf(
17727 '<a href="%s" target="_blank">%s</a>',
17728 'https://freemius.com',
17729 'freemius.com'
17730 )
17731 )
17732 );
17733 }
17734 }
17735 }
17736 }
17737 }
17738
17739 break;
17740
17741 case 'delete_account':
17742 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
17743
17744 if ( $plugin_id == $this->get_id() ) {
17745 if ( $is_network_action && empty( $blog_id ) ) {
17746 $this->delete_network_account_event();
17747 } else {
17748 $this->delete_account_event();
17749 }
17750
17751 // Clear user and site.
17752 $this->_site = null;
17753 $this->_user = null;
17754
17755 fs_redirect( $this->get_activation_url() );
17756 } else {
17757 if ( $this->is_addon_activated( $plugin_id ) ) {
17758 $fs_addon = self::get_instance_by_id( $plugin_id );
17759 $fs_addon->delete_account_event();
17760
17761 fs_redirect( $this->_get_admin_page_url( 'account' ) );
17762 }
17763 }
17764
17765 return;
17766
17767 case 'downgrade_account':
17768 if ( is_numeric( $blog_id ) ) {
17769 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
17770 } else {
17771 check_admin_referer( $action );
17772 }
17773
17774 if ( $plugin_id == $this->get_id() ) {
17775 $this->_downgrade_site();
17776
17777 if ( is_numeric( $blog_id ) ) {
17778 $this->switch_to_blog( $this->_storage->network_install_blog_id );
17779 }
17780 } else if ( $this->is_addon_activated( $plugin_id ) ) {
17781 $fs_addon = self::get_instance_by_id( $plugin_id );
17782 $fs_addon->_downgrade_site();
17783 }
17784
17785 return;
17786
17787 case 'activate_license':
17788 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
17789
17790 $fs = $this;
17791 if ( $plugin_id != $this->get_id() ) {
17792 $fs = $this->is_addon_activated( $plugin_id ) ?
17793 self::get_instance_by_id( $plugin_id ) :
17794 null;
17795 }
17796
17797 if ( is_object( $fs ) ) {
17798 $fs->_activate_license();
17799 }
17800
17801 return;
17802
17803 case 'deactivate_license':
17804 check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) );
17805
17806 if ( $plugin_id == $this->get_id() ) {
17807 $this->_deactivate_license();
17808
17809 if ( $this->is_only_premium() ) {
17810 // Clear user and site.
17811 $this->_site = null;
17812 $this->_user = null;
17813
17814 fs_redirect( $this->get_activation_url() );
17815 }
17816 } else {
17817 if ( $this->is_addon_activated( $plugin_id ) ) {
17818 $fs_addon = self::get_instance_by_id( $plugin_id );
17819 $fs_addon->_deactivate_license();
17820 }
17821 }
17822
17823 return;
17824
17825 case 'check_updates':
17826 check_admin_referer( $action );
17827 $this->check_updates();
17828
17829 return;
17830
17831 case 'change_owner':
17832 $state = fs_request_get( 'state', 'init' );
17833 switch ( $state ) {
17834 case 'init':
17835 $candidate_email = fs_request_get( 'candidate_email', '' );
17836
17837 if ( $this->init_change_owner( $candidate_email ) ) {
17838 $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>' ) );
17839 }
17840 break;
17841 case 'owner_confirmed':
17842 $candidate_email = fs_request_get( 'candidate_email', '' );
17843
17844 $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>' ) );
17845 break;
17846 case 'candidate_confirmed':
17847 if ( $this->complete_change_owner() ) {
17848 $this->_admin_notices->add_sticky(
17849 sprintf( $this->get_text_inline( '%s is the new owner of the account.', 'change-owner-request_candidate-confirmed' ), '<b>' . $this->_user->email . '</b>' ),
17850 'ownership_changed',
17851 $this->get_text_x_inline( 'Congrats', 'as congratulations', 'congrats' ) . '!'
17852 );
17853 } else {
17854 // @todo Handle failed ownership change message.
17855 }
17856 break;
17857 }
17858
17859 return;
17860
17861 case 'update_email':
17862 check_admin_referer( 'update_email' );
17863
17864 $new_email = fs_request_get( 'fs_email_' . $this->get_unique_affix(), '' );
17865 $result = $this->update_email( $new_email );
17866
17867 if ( isset( $result->error ) ) {
17868 switch ( $result->error->code ) {
17869 case 'user_exist':
17870 $this->_admin_notices->add(
17871 $this->get_text_inline( 'Sorry, we could not complete the email update. Another user with the same email is already registered.', 'user-exist-message' ) . ' ' .
17872 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>' ) .
17873 sprintf(
17874 '<a style="margin-left: 10px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
17875 $this->get_account_url( 'change_owner', array(
17876 'state' => 'init',
17877 'candidate_email' => $new_email
17878 ) ),
17879 $this->get_text_inline( 'Change Ownership', 'change-ownership' )
17880 ),
17881 $oops_text,
17882 'error'
17883 );
17884 break;
17885 }
17886 } else {
17887 $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' ) );
17888 }
17889
17890 return;
17891
17892 case 'update_user_name':
17893 check_admin_referer( 'update_user_name' );
17894
17895 $result = $this->update_user_name();
17896
17897 if ( isset( $result->error ) ) {
17898 $this->_admin_notices->add(
17899 $this->get_text_inline( 'Please provide your full name.', 'name-update-failed-message' ),
17900 $oops_text,
17901 'error'
17902 );
17903 } else {
17904 $this->_admin_notices->add( $this->get_text_inline( 'Your name was successfully updated.', 'name-updated-message' ) );
17905 }
17906
17907 return;
17908
17909 #region Actions that might be called from external links (e.g. email)
17910
17911 case 'cancel_trial':
17912 if ( $plugin_id == $this->get_id() ) {
17913 $this->_cancel_trial();
17914 } else {
17915 if ( $this->is_addon_activated( $plugin_id ) ) {
17916 $fs_addon = self::get_instance_by_id( $plugin_id );
17917 $fs_addon->_cancel_trial();
17918 }
17919 }
17920
17921 return;
17922
17923 case 'verify_email':
17924 $this->verify_email();
17925
17926 return;
17927
17928 case 'sync_user':
17929 $this->_handle_account_user_sync();
17930
17931 return;
17932
17933 case $this->get_unique_affix() . '_sync_license':
17934 $this->_sync_license();
17935
17936 return;
17937
17938 case 'download_latest':
17939 $this->download_latest_directly( $plugin_id );
17940
17941 return;
17942
17943 #endregion
17944 }
17945
17946 if ( WP_FS__IS_POST_REQUEST ) {
17947 $properties = array( 'site_secret_key', 'site_id', 'site_public_key' );
17948 foreach ( $properties as $p ) {
17949 if ( 'update_' . $p === $action ) {
17950 check_admin_referer( $action );
17951
17952 $this->_logger->log( $action );
17953
17954 $site_property = substr( $p, strlen( 'site_' ) );
17955 $site_property_value = fs_request_get( 'fs_' . $p . '_' . $this->get_unique_affix(), '' );
17956 $this->get_site()->{$site_property} = $site_property_value;
17957
17958 // Store account after modification.
17959 $this->_store_site();
17960
17961 $this->do_action( 'account_property_edit', 'site', $site_property, $site_property_value );
17962
17963 $this->_admin_notices->add( sprintf(
17964 /* translators: %s: User's account property (e.g. email address, name) */
17965 $this->get_text_inline( 'You have successfully updated your %s.', 'x-updated' ),
17966 '<b>' . str_replace( '_', ' ', $p ) . '</b>'
17967 ) );
17968
17969 return;
17970 }
17971 }
17972 }
17973 }
17974
17975 /**
17976 * Account page resources load.
17977 *
17978 * @author Vova Feldman (@svovaf)
17979 * @since 1.0.6
17980 */
17981 function _account_page_load() {
17982 $this->_logger->entrance();
17983
17984 $this->_logger->info( var_export( $_REQUEST, true ) );
17985
17986 fs_enqueue_local_style( 'fs_account', '/admin/account.css' );
17987
17988 if ( $this->has_addons() ) {
17989 wp_enqueue_script( 'plugin-install' );
17990 add_thickbox();
17991
17992 function fs_addons_body_class( $classes ) {
17993 $classes .= ' plugins-php';
17994
17995 return $classes;
17996 }
17997
17998 add_filter( 'admin_body_class', 'fs_addons_body_class' );
17999 }
18000
18001 if ( $this->has_paid_plan() &&
18002 ! $this->has_any_license() &&
18003 ! $this->is_sync_executed() &&
18004 $this->is_tracking_allowed()
18005 ) {
18006 /**
18007 * If no licenses found and no sync job was executed during the last 24 hours,
18008 * just execute the sync job right away (blocking execution).
18009 *
18010 * @since 1.1.7.3
18011 */
18012 $this->run_manual_sync();
18013 }
18014
18015 $this->_handle_account_edits();
18016
18017 $this->do_action( 'account_page_load_before_departure' );
18018 }
18019
18020 /**
18021 * Renders the "Affiliation" page.
18022 *
18023 * @author Leo Fajardo (@leorw)
18024 * @since 1.2.3
18025 */
18026 function _affiliation_page_render() {
18027 $this->_logger->entrance();
18028
18029 $this->fetch_affiliate_and_terms();
18030
18031 fs_enqueue_local_style( 'fs_affiliation', '/admin/affiliation.css' );
18032
18033 $vars = array( 'id' => $this->_module_id );
18034 echo $this->apply_filters( "/forms/affiliation.php", fs_get_template( '/forms/affiliation.php', $vars ) );
18035 }
18036
18037
18038 /**
18039 * Render account page.
18040 *
18041 * @author Vova Feldman (@svovaf)
18042 * @since 1.0.0
18043 */
18044 function _account_page_render() {
18045 $this->_logger->entrance();
18046
18047 $template = 'account.php';
18048 $vars = array( 'id' => $this->_module_id );
18049
18050 /**
18051 * Added filter to the template to allow developers wrapping the template
18052 * in custom HTML (e.g. within a wizard/tabs).
18053 *
18054 * @author Vova Feldman (@svovaf)
18055 * @since 1.2.1.6
18056 */
18057 echo $this->apply_filters( "templates/{$template}", fs_get_template( $template, $vars ) );
18058 }
18059
18060 /**
18061 * Render account connect page.
18062 *
18063 * @author Vova Feldman (@svovaf)
18064 * @since 1.0.7
18065 */
18066 function _connect_page_render() {
18067 $this->_logger->entrance();
18068
18069 $vars = array( 'id' => $this->_module_id );
18070
18071 /**
18072 * Added filter to the template to allow developers wrapping the template
18073 * in custom HTML (e.g. within a wizard/tabs).
18074 *
18075 * @author Vova Feldman (@svovaf)
18076 * @since 1.2.1.6
18077 */
18078 echo $this->apply_filters( 'templates/connect.php', fs_get_template( 'connect.php', $vars ) );
18079 }
18080
18081 /**
18082 * Load required resources before add-ons page render.
18083 *
18084 * @author Vova Feldman (@svovaf)
18085 * @since 1.0.6
18086 */
18087 function _addons_page_load() {
18088 $this->_logger->entrance();
18089
18090 fs_enqueue_local_style( 'fs_addons', '/admin/add-ons.css' );
18091
18092 wp_enqueue_script( 'plugin-install' );
18093 add_thickbox();
18094
18095 function fs_addons_body_class( $classes ) {
18096 $classes .= ' plugins-php';
18097
18098 return $classes;
18099 }
18100
18101 add_filter( 'admin_body_class', 'fs_addons_body_class' );
18102
18103 if ( ! $this->is_registered() && $this->is_org_repo_compliant() ) {
18104 $this->_admin_notices->add(
18105 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>' ),
18106 $this->get_text_x_inline( 'Heads up', 'advance notice of something that will need attention.', 'heads-up' ),
18107 'update-nag'
18108 );
18109 }
18110 }
18111
18112 /**
18113 * Render add-ons page.
18114 *
18115 * @author Vova Feldman (@svovaf)
18116 * @since 1.0.6
18117 */
18118 function _addons_page_render() {
18119 $this->_logger->entrance();
18120
18121 $vars = array( 'id' => $this->_module_id );
18122
18123 /**
18124 * Added filter to the template to allow developers wrapping the template
18125 * in custom HTML (e.g. within a wizard/tabs).
18126 *
18127 * @author Vova Feldman (@svovaf)
18128 * @since 1.2.1.6
18129 */
18130 echo $this->apply_filters( 'templates/add-ons.php', fs_get_template( 'add-ons.php', $vars ) );
18131 }
18132
18133 /* Pricing & Upgrade
18134 ------------------------------------------------------------------------------------------------------------------*/
18135 /**
18136 * Render pricing page.
18137 *
18138 * @author Vova Feldman (@svovaf)
18139 * @since 1.0.0
18140 */
18141 function _pricing_page_render() {
18142 $this->_logger->entrance();
18143
18144 $vars = array( 'id' => $this->_module_id );
18145
18146 if ( 'true' === fs_request_get( 'checkout', false ) ) {
18147 fs_require_once_template( 'checkout.php', $vars );
18148 } else {
18149 fs_require_once_template( 'pricing.php', $vars );
18150 }
18151 }
18152
18153 #----------------------------------------------------------------------------------
18154 #region Contact Us
18155 #----------------------------------------------------------------------------------
18156
18157 /**
18158 * Render contact-us page.
18159 *
18160 * @author Vova Feldman (@svovaf)
18161 * @since 1.0.3
18162 */
18163 function _contact_page_render() {
18164 $this->_logger->entrance();
18165
18166 $vars = array( 'id' => $this->_module_id );
18167 fs_require_once_template( 'contact.php', $vars );
18168 }
18169
18170 #endregion ------------------------------------------------------------------------
18171
18172 /**
18173 * Hide all admin notices to prevent distractions.
18174 *
18175 * @author Vova Feldman (@svovaf)
18176 * @since 1.0.3
18177 *
18178 * @uses remove_all_actions()
18179 */
18180 private static function _hide_admin_notices() {
18181 remove_all_actions( 'admin_notices' );
18182 remove_all_actions( 'network_admin_notices' );
18183 remove_all_actions( 'all_admin_notices' );
18184 remove_all_actions( 'user_admin_notices' );
18185 }
18186
18187 static function _clean_admin_content_section_hook() {
18188 self::_hide_admin_notices();
18189
18190 // Hide footer.
18191 echo '<style>#wpfooter { display: none !important; }</style>';
18192 }
18193
18194 /**
18195 * Attach to admin_head hook to hide all admin notices.
18196 *
18197 * @author Vova Feldman (@svovaf)
18198 * @since 1.0.3
18199 */
18200 static function _clean_admin_content_section() {
18201 add_action( 'admin_head', 'Freemius::_clean_admin_content_section_hook' );
18202 }
18203
18204 /* CSS & JavaScript
18205 ------------------------------------------------------------------------------------------------------------------*/
18206 /* function _enqueue_script($handle, $src) {
18207 $url = plugins_url( substr( WP_FS__DIR_JS, strlen( $this->_plugin_dir_path ) ) . '/assets/js/' . $src );
18208
18209 $this->_logger->entrance( 'script = ' . $url );
18210
18211 wp_enqueue_script( $handle, $url );
18212 }*/
18213
18214 /* SDK
18215 ------------------------------------------------------------------------------------------------------------------*/
18216 private $_user_api;
18217
18218 /**
18219 *
18220 * @author Vova Feldman (@svovaf)
18221 * @since 1.0.2
18222 *
18223 * @param bool $flush
18224 *
18225 * @return FS_Api
18226 */
18227 private function get_api_user_scope( $flush = false ) {
18228 if ( ! isset( $this->_user_api ) || $flush ) {
18229 $this->_user_api = $this->get_api_user_scope_by_user( $this->_user );
18230 }
18231
18232 return $this->_user_api;
18233 }
18234
18235 /**
18236 * @author Vova Feldman (@svovaf)
18237 * @since 2.0.0
18238 *
18239 * @param \FS_User $user
18240 *
18241 * @return \FS_Api
18242 */
18243 private function get_api_user_scope_by_user( FS_User $user ) {
18244 return FS_Api::instance(
18245 $this->_module_id,
18246 'user',
18247 $user->id,
18248 $user->public_key,
18249 ! $this->is_live(),
18250 $user->secret_key
18251 );
18252 }
18253
18254 /**
18255 *
18256 * @author Leo Fajardo (@leorw)
18257 * @since 2.0.0
18258 *
18259 * @param bool $flush
18260 *
18261 * @return FS_Api
18262 */
18263 private function get_current_or_network_user_api_scope( $flush = false ) {
18264 if ( ! $this->_is_network_active || isset( $this->_user ) ) {
18265 return $this->get_api_user_scope( $flush );
18266 }
18267
18268 $user = $this->get_current_or_network_user();
18269
18270 $this->_user_api = FS_Api::instance(
18271 $this->_module_id,
18272 'user',
18273 $user->id,
18274 $user->public_key,
18275 ! $this->is_live(),
18276 $user->secret_key
18277 );
18278
18279 return $this->_user_api;
18280 }
18281
18282 private $_site_api;
18283
18284 /**
18285 *
18286 * @author Vova Feldman (@svovaf)
18287 * @since 1.0.2
18288 *
18289 * @param bool $flush
18290 *
18291 * @return FS_Api
18292 */
18293 private function get_api_site_scope( $flush = false ) {
18294 if ( ! isset( $this->_site_api ) || $flush ) {
18295 $this->_site_api = FS_Api::instance(
18296 $this->_module_id,
18297 'install',
18298 $this->_site->id,
18299 $this->_site->public_key,
18300 ! $this->is_live(),
18301 $this->_site->secret_key
18302 );
18303 }
18304
18305 return $this->_site_api;
18306 }
18307
18308 private $_plugin_api;
18309
18310 /**
18311 * Get plugin public API scope.
18312 *
18313 * @author Vova Feldman (@svovaf)
18314 * @since 1.0.7
18315 *
18316 * @return FS_Api
18317 */
18318 function get_api_plugin_scope() {
18319 if ( ! isset( $this->_plugin_api ) ) {
18320 $this->_plugin_api = FS_Api::instance(
18321 $this->_module_id,
18322 'plugin',
18323 $this->_plugin->id,
18324 $this->_plugin->public_key,
18325 ! $this->is_live()
18326 );
18327 }
18328
18329 return $this->_plugin_api;
18330 }
18331
18332 /**
18333 * Get site API scope object (fallback to public plugin scope when not registered).
18334 *
18335 * @author Vova Feldman (@svovaf)
18336 * @since 1.0.7
18337 *
18338 * @return FS_Api
18339 */
18340 function get_api_site_or_plugin_scope() {
18341 return $this->is_registered() ?
18342 $this->get_api_site_scope() :
18343 $this->get_api_plugin_scope();
18344 }
18345
18346 /**
18347 * Show trial promotional notice (if any trial exist).
18348 *
18349 * @author Vova Feldman (@svovaf)
18350 * @since 1.0.9
18351 *
18352 * @param $plans
18353 */
18354 function _check_for_trial_plans( $plans ) {
18355 $this->_storage->has_trial_plan = FS_Plan_Manager::instance()->has_trial_plan( $plans );
18356 }
18357
18358 /**
18359 * During trial promotion the "upgrade" submenu item turns to
18360 * "start trial" to encourage the trial. Since we want to keep
18361 * the same menu item handler and there's no robust way to
18362 * add new arguments to the menu item link's querystring,
18363 * use JavaScript to find the menu item and update the href of
18364 * the link.
18365 *
18366 * @author Vova Feldman (@svovaf)
18367 * @since 1.2.1.5
18368 */
18369 function _fix_start_trial_menu_item_url() {
18370 $template_args = array( 'id' => $this->_module_id );
18371 fs_require_template( 'add-trial-to-pricing.php', $template_args );
18372 }
18373
18374 /**
18375 * Check if module is currently in a trial promotion mode.
18376 *
18377 * @author Vova Feldman (@svovaf)
18378 * @since 1.2.2.7
18379 *
18380 * @return bool
18381 */
18382 function is_in_trial_promotion() {
18383 return $this->_admin_notices->has_sticky( 'trial_promotion' );
18384 }
18385
18386 /**
18387 * Show trial promotional notice (if any trial exist).
18388 *
18389 * @author Vova Feldman (@svovaf)
18390 * @since 1.0.9
18391 *
18392 * @return bool If trial notice added.
18393 */
18394 function _add_trial_notice() {
18395 if ( ! $this->is_user_admin() ) {
18396 return false;
18397 }
18398
18399 if ( ! $this->is_user_in_admin() ) {
18400 return false;
18401 }
18402
18403 if ( $this->_is_network_active ) {
18404 if ( fs_is_network_admin() ) {
18405 // Network level trial is disabled at the moment.
18406 return false;
18407 }
18408
18409 if ( ! $this->is_delegated_connection() ) {
18410 // Only delegated sites should support trials.
18411 return false;
18412 }
18413 }
18414
18415 // Check if trial message is already shown.
18416 if ( $this->is_in_trial_promotion() ) {
18417 add_action( 'admin_footer', array( &$this, '_fix_start_trial_menu_item_url' ) );
18418
18419 $this->_menu->add_counter_to_menu_item( 1, 'fs-trial' );
18420
18421 return false;
18422 }
18423
18424 if ( $this->is_premium() && ! WP_FS__DEV_MODE ) {
18425 // Don't show trial if running the premium code, unless running in DEV mode.
18426 return false;
18427 }
18428
18429 if ( ! $this->has_trial_plan() ) {
18430 // No plans with trial.
18431 return false;
18432 }
18433
18434 if ( ! $this->apply_filters( 'show_trial', true ) ) {
18435 // Developer explicitly asked not to show the trial promo.
18436 return false;
18437 }
18438
18439 if ( $this->is_registered() ) {
18440 // Check if trial already utilized.
18441 if ( $this->_site->is_trial_utilized() ) {
18442 return false;
18443 }
18444
18445 if ( $this->is_paying_or_trial() ) {
18446 // Don't show trial if paying or already in trial.
18447 return false;
18448 }
18449 }
18450
18451 if ( $this->is_activation_mode() || $this->is_pending_activation() ) {
18452 // If not yet opted-in/skipped, or pending activation, don't show trial.
18453 return false;
18454 }
18455
18456 $last_time_trial_promotion_shown = $this->_storage->get( 'trial_promotion_shown', false );
18457 $was_promotion_shown_before = ( false !== $last_time_trial_promotion_shown );
18458
18459 // Show promotion if never shown before and 24 hours after initial activation with FS.
18460 if ( ! $was_promotion_shown_before &&
18461 $this->_storage->install_timestamp > ( time() - WP_FS__TIME_24_HOURS_IN_SEC )
18462 ) {
18463 return false;
18464 }
18465
18466 // OR if promotion was shown before, try showing it every 30 days.
18467 if ( $was_promotion_shown_before &&
18468 30 * WP_FS__TIME_24_HOURS_IN_SEC > time() - $last_time_trial_promotion_shown
18469 ) {
18470 return false;
18471 }
18472
18473 $trial_period = $this->_trial_days;
18474 $require_payment = $this->_is_trial_require_payment;
18475 $trial_url = $this->get_trial_url();
18476 $plans_string = strtolower( $this->get_text_inline( 'Awesome', 'awesome' ) );
18477
18478 if ( $this->is_registered() ) {
18479 // If opted-in, override trial with up to date data from API.
18480 $trial_plans = FS_Plan_Manager::instance()->get_trial_plans( $this->_plans );
18481 $trial_plans_count = count( $trial_plans );
18482
18483 if ( 0 === $trial_plans_count ) {
18484 // If there's no plans with a trial just exit.
18485 return false;
18486 }
18487
18488 /**
18489 * @var FS_Plugin_Plan $paid_plan
18490 */
18491 $paid_plan = $trial_plans[0];
18492 $require_payment = $paid_plan->is_require_subscription;
18493 $trial_period = $paid_plan->trial_period;
18494
18495 $total_paid_plans = count( $this->_plans ) - ( FS_Plan_Manager::instance()->has_free_plan( $this->_plans ) ? 1 : 0 );
18496
18497 if ( $total_paid_plans !== $trial_plans_count ) {
18498 // Not all paid plans have a trial - generate a string of those that have it.
18499 for ( $i = 0; $i < $trial_plans_count; $i ++ ) {
18500 $plans_string .= sprintf(
18501 ' <a href="%s">%s</a>',
18502 $trial_url,
18503 $trial_plans[ $i ]->title
18504 );
18505
18506 if ( $i < $trial_plans_count - 2 ) {
18507 $plans_string .= ', ';
18508 } else if ( $i == $trial_plans_count - 2 ) {
18509 $plans_string .= ' and ';
18510 }
18511 }
18512 }
18513 }
18514
18515 $message = sprintf(
18516 $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' ),
18517 sprintf( '<b>%s</b>', $this->get_plugin_name() ),
18518 $plans_string,
18519 $trial_period
18520 );
18521
18522 // "No Credit-Card Required" or "No Commitment for N Days".
18523 $cc_string = $require_payment ?
18524 sprintf( $this->get_text_inline( 'No commitment for %s days - cancel anytime!', 'no-commitment-for-x-days' ), $trial_period ) :
18525 $this->get_text_inline( 'No credit card required', 'no-cc-required' ) . '!';
18526
18527
18528 // Start trial button.
18529 $button = ' ' . sprintf(
18530 '<a style="margin-left: 10px; vertical-align: super;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
18531 $trial_url,
18532 $this->get_text_x_inline( 'Start free trial', 'call to action', 'start-free-trial' )
18533 );
18534
18535 $this->_admin_notices->add_sticky(
18536 $this->apply_filters( 'trial_promotion_message', "{$message} {$cc_string} {$button}" ),
18537 'trial_promotion',
18538 '',
18539 'promotion'
18540 );
18541
18542 $this->_storage->trial_promotion_shown = WP_FS__SCRIPT_START_TIME;
18543
18544 return true;
18545 }
18546
18547 /**
18548 * Lets users/customers know that the product has an affiliate program.
18549 *
18550 * @author Leo Fajardo (@leorw)
18551 * @since 1.2.2.11
18552 *
18553 * @return bool Returns true if the notice has been added.
18554 */
18555 function _add_affiliate_program_notice() {
18556 if ( ! $this->is_user_admin() ) {
18557 return false;
18558 }
18559
18560 if ( ! $this->is_user_in_admin() ) {
18561 return false;
18562 }
18563
18564 // Check if the notice is already shown.
18565 if ( $this->_admin_notices->has_sticky( 'affiliate_program' ) ) {
18566 return false;
18567 }
18568
18569 if (
18570 // Product has no affiliate program.
18571 ! $this->has_affiliate_program() ||
18572 // User has applied for an affiliate account.
18573 ! empty( $this->_storage->affiliate_application_data )
18574 ) {
18575 return false;
18576 }
18577
18578 if ( ! $this->apply_filters( 'show_affiliate_program_notice', true ) ) {
18579 // Developer explicitly asked not to show the notice about the affiliate program.
18580 return false;
18581 }
18582
18583 if ( $this->is_activation_mode() || $this->is_pending_activation() ) {
18584 // If not yet opted in/skipped, or pending activation, don't show the notice.
18585 return false;
18586 }
18587
18588 $last_time_notice_was_shown = $this->_storage->get( 'affiliate_program_notice_shown', false );
18589 $was_notice_shown_before = ( false !== $last_time_notice_was_shown );
18590
18591 /**
18592 * Do not show the notice if it was already shown before or less than 30 days have passed since the initial
18593 * activation with FS.
18594 */
18595 if ( $was_notice_shown_before ||
18596 $this->_storage->install_timestamp > ( time() - ( WP_FS__TIME_24_HOURS_IN_SEC * 30 ) )
18597 ) {
18598 return false;
18599 }
18600
18601 if ( ! $this->is_paying() &&
18602 FS_Plugin::AFFILIATE_MODERATION_CUSTOMERS == $this->_plugin->affiliate_moderation
18603 ) {
18604 // If the user is not a customer and the affiliate program is only for customers, don't show the notice.
18605 return false;
18606 }
18607
18608 $message = sprintf(
18609 $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' ),
18610 sprintf( '<strong>%s</strong>', $this->get_plugin_name() ),
18611 $this->get_module_label( true )
18612 );
18613
18614 // HTML code for the "Learn more..." button.
18615 $button = ' ' . sprintf(
18616 '<a style="display: block; margin-top: 10px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
18617 $this->_get_admin_page_url( 'affiliation' ),
18618 $this->get_text_inline( 'Learn more', 'learn-more' ) . '...'
18619 );
18620
18621 $this->_admin_notices->add_sticky(
18622 $this->apply_filters( 'affiliate_program_notice', "{$message} {$button}" ),
18623 'affiliate_program',
18624 '',
18625 'promotion'
18626 );
18627
18628 $this->_storage->affiliate_program_notice_shown = WP_FS__SCRIPT_START_TIME;
18629
18630 return true;
18631 }
18632
18633 /**
18634 * @author Vova Feldman (@svovaf)
18635 * @since 1.2.1.5
18636 */
18637 function _enqueue_common_css() {
18638 if ( $this->has_paid_plan() && ! $this->is_paying() ) {
18639 // Add basic CSS for admin-notices and menu-item colors.
18640 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
18641 }
18642 }
18643
18644 /**
18645 * @author Leo Fajardo (@leorw)
18646 * @since 1.2.2
18647 */
18648 function _show_theme_activation_optin_dialog() {
18649 fs_enqueue_local_style( 'fs_connect', '/admin/connect.css' );
18650
18651 add_action( 'admin_footer-themes.php', array( &$this, '_add_fs_theme_activation_dialog' ) );
18652 }
18653
18654 /**
18655 * @author Leo Fajardo (@leorw)
18656 * @since 1.2.2
18657 */
18658 function _add_fs_theme_activation_dialog() {
18659 $vars = array( 'id' => $this->_module_id );
18660 fs_require_once_template( 'connect.php', $vars );
18661 }
18662
18663 /* Action Links
18664 ------------------------------------------------------------------------------------------------------------------*/
18665 private $_action_links_hooked = false;
18666 private $_action_links = array();
18667
18668 /**
18669 * Hook to plugin action links filter.
18670 *
18671 * @author Vova Feldman (@svovaf)
18672 * @since 1.0.0
18673 */
18674 private function hook_plugin_action_links() {
18675 $this->_logger->entrance();
18676
18677 $this->_action_links_hooked = true;
18678
18679 $this->_logger->log( 'Adding action links hooks.' );
18680
18681 // Add action link to settings page.
18682 add_filter( 'plugin_action_links_' . $this->_plugin_basename, array(
18683 &$this,
18684 '_modify_plugin_action_links_hook'
18685 ), WP_FS__DEFAULT_PRIORITY, 2 );
18686 add_filter( 'network_admin_plugin_action_links_' . $this->_plugin_basename, array(
18687 &$this,
18688 '_modify_plugin_action_links_hook'
18689 ), WP_FS__DEFAULT_PRIORITY, 2 );
18690 }
18691
18692 /**
18693 * Add plugin action link.
18694 *
18695 * @author Vova Feldman (@svovaf)
18696 * @since 1.0.0
18697 *
18698 * @param $label
18699 * @param $url
18700 * @param bool $external
18701 * @param int $priority
18702 * @param bool $key
18703 */
18704 function add_plugin_action_link( $label, $url, $external = false, $priority = WP_FS__DEFAULT_PRIORITY, $key = false ) {
18705 $this->_logger->entrance();
18706
18707 if ( ! isset( $this->_action_links[ $priority ] ) ) {
18708 $this->_action_links[ $priority ] = array();
18709 }
18710
18711 if ( false === $key ) {
18712 $key = preg_replace( "/[^A-Za-z0-9 ]/", '', strtolower( $label ) );
18713 }
18714
18715 $this->_action_links[ $priority ][] = array(
18716 'label' => $label,
18717 'href' => $url,
18718 'key' => $key,
18719 'external' => $external
18720 );
18721 }
18722
18723 /**
18724 * Adds Upgrade and Add-Ons links to the main Plugins page link actions collection.
18725 *
18726 * @author Vova Feldman (@svovaf)
18727 * @since 1.0.0
18728 */
18729 function _add_upgrade_action_link() {
18730 $this->_logger->entrance();
18731
18732 if ( $this->is_registered() ) {
18733 if ( ! $this->is_paying() && $this->has_paid_plan() ) {
18734 $this->add_plugin_action_link(
18735 $this->get_text_inline( 'Upgrade', 'upgrade' ),
18736 $this->get_upgrade_url(),
18737 false,
18738 7,
18739 'upgrade'
18740 );
18741 }
18742
18743 if ( $this->has_addons() ) {
18744 $this->add_plugin_action_link(
18745 $this->get_text_inline( 'Add-Ons', 'add-ons' ),
18746 $this->_get_admin_page_url( 'addons' ),
18747 false,
18748 9,
18749 'addons'
18750 );
18751 }
18752 }
18753 }
18754
18755 /**
18756 * Adds "Activate License" or "Change License" link to the main Plugins page link actions collection.
18757 *
18758 * @author Leo Fajardo (@leorw)
18759 * @since 1.1.9
18760 */
18761 function _add_license_action_link() {
18762 $this->_logger->entrance();
18763
18764 if ( ! self::is_ajax() ) {
18765 // Inject license activation dialog UI and client side code.
18766 add_action( 'admin_footer', array( &$this, '_add_license_activation_dialog_box' ) );
18767 }
18768
18769 $link_text = $this->is_free_plan() ?
18770 $this->get_text_inline( 'Activate License', 'activate-license' ) :
18771 $this->get_text_inline( 'Change License', 'change-license' );
18772
18773 $this->add_plugin_action_link(
18774 $link_text,
18775 '#',
18776 false,
18777 11,
18778 ( 'activate-license ' . $this->get_unique_affix() )
18779 );
18780 }
18781
18782 /**
18783 * Adds "Opt in" or "Opt out" link to the main "Plugins" page link actions collection.
18784 *
18785 * @author Leo Fajardo (@leorw)
18786 * @since 1.2.1.5
18787 */
18788 function _add_tracking_links() {
18789 if ( ! current_user_can( 'manage_options' ) ) {
18790 return;
18791 }
18792
18793 $this->_logger->entrance();
18794
18795 /**
18796 * If the activation has been delegated to site admins, no tracking-related actions for now.
18797 *
18798 * @author Leo Fajardo (@leorw)
18799 */
18800 if ( $this->_is_network_active && $this->is_network_delegated_connection() ) {
18801 return;
18802 }
18803
18804 if ( fs_request_is_action_secure( $this->get_unique_affix() . '_reconnect' ) ) {
18805 if ( ! $this->is_registered() && $this->is_anonymous() ) {
18806 $this->connect_again();
18807
18808 return;
18809 }
18810 }
18811
18812 if ( ( $this->is_plugin() && ! self::is_plugins_page() ) ||
18813 ( $this->is_theme() && ! self::is_themes_page() )
18814 ) {
18815 // Only show tracking links on the plugins and themes pages.
18816 return;
18817 }
18818
18819 if ( ! $this->is_enable_anonymous() ) {
18820 // Don't allow to opt-out if anonymous mode is disabled.
18821 return;
18822 }
18823
18824 if ( ! $this->is_free_plan() ) {
18825 // Don't allow to opt-out if running in paid plan.
18826 return;
18827 }
18828
18829 if ( $this->add_ajax_action( 'stop_tracking', array( &$this, '_stop_tracking_callback' ) ) ) {
18830 return;
18831 }
18832
18833 if ( $this->add_ajax_action( 'allow_tracking', array( &$this, '_allow_tracking_callback' ) ) ) {
18834 return;
18835 }
18836
18837 $url = '#';
18838
18839 if ( $this->is_registered() ) {
18840 if ( $this->is_tracking_allowed() ) {
18841 $link_text_id = $this->get_text_inline( 'Opt Out', 'opt-out' );
18842 } else {
18843 $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' );
18844 }
18845
18846 add_action( 'admin_footer', array( &$this, '_add_optout_dialog' ) );
18847 } else {
18848 $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' );
18849
18850 $params = ! $this->is_anonymous() ?
18851 array() :
18852 array(
18853 'nonce' => wp_create_nonce( $this->get_unique_affix() . '_reconnect' ),
18854 'fs_action' => ( $this->get_unique_affix() . '_reconnect' ),
18855 );
18856
18857 $url = $this->get_activation_url( $params );
18858 }
18859
18860 if ( $this->is_plugin() && self::is_plugins_page() ) {
18861 $this->add_plugin_action_link(
18862 $link_text_id,
18863 $url,
18864 false,
18865 13,
18866 "opt-in-or-opt-out {$this->_slug}"
18867 );
18868 }
18869 }
18870
18871 /**
18872 * Get the URL of the page that should be loaded right after the plugin activation.
18873 *
18874 * @author Vova Feldman (@svovaf)
18875 * @since 1.1.7.4
18876 *
18877 * @return string
18878 */
18879 function get_after_plugin_activation_redirect_url() {
18880 $url = false;
18881
18882 if ( ! $this->is_addon() || ! $this->has_free_plan() ) {
18883 $first_time_path = $this->_menu->get_first_time_path();
18884 $url = $this->is_activation_mode() ?
18885 $this->get_activation_url() :
18886 ( empty( $first_time_path ) ?
18887 $this->_get_admin_page_url() :
18888 $first_time_path );
18889 } else {
18890 $plugin_fs = false;
18891
18892 if ( $this->is_parent_plugin_installed() ) {
18893 $plugin_fs = self::get_parent_instance();
18894 }
18895
18896 if ( is_object( $plugin_fs ) ) {
18897 if ( ! $plugin_fs->is_registered() ) {
18898 // Forward to parent plugin connect when parent not registered.
18899 $url = $plugin_fs->get_activation_url();
18900 } else {
18901 // Forward to account page.
18902 $url = $plugin_fs->_get_admin_page_url( 'account' );
18903 }
18904 }
18905 }
18906
18907 return $url;
18908 }
18909
18910 /**
18911 * Forward page to activation page.
18912 *
18913 * @author Vova Feldman (@svovaf)
18914 * @since 1.0.3
18915 */
18916 function _redirect_on_activation_hook() {
18917 $url = $this->get_after_plugin_activation_redirect_url();
18918
18919 if ( is_string( $url ) ) {
18920 fs_redirect( $url );
18921 }
18922 }
18923
18924 /**
18925 * Modify plugin's page action links collection.
18926 *
18927 * @author Vova Feldman (@svovaf)
18928 * @since 1.0.0
18929 *
18930 * @param array $links
18931 * @param $file
18932 *
18933 * @return array
18934 */
18935 function _modify_plugin_action_links_hook( $links, $file ) {
18936 $this->_logger->entrance();
18937
18938 $passed_deactivate = false;
18939 $deactivate_link = '';
18940 $before_deactivate = array();
18941 $after_deactivate = array();
18942 foreach ( $links as $key => $link ) {
18943 if ( 'deactivate' === $key ) {
18944 $deactivate_link = $link;
18945 $passed_deactivate = true;
18946 continue;
18947 }
18948
18949 if ( ! $passed_deactivate ) {
18950 $before_deactivate[ $key ] = $link;
18951 } else {
18952 $after_deactivate[ $key ] = $link;
18953 }
18954 }
18955
18956 ksort( $this->_action_links );
18957
18958 foreach ( $this->_action_links as $new_links ) {
18959 foreach ( $new_links as $link ) {
18960 $before_deactivate[ $link['key'] ] = '<a href="' . $link['href'] . '"' . ( $link['external'] ? ' target="_blank"' : '' ) . '>' . $link['label'] . '</a>';
18961 }
18962 }
18963
18964 if ( ! empty( $deactivate_link ) ) {
18965 /**
18966 * This HTML element is used to identify the correct plugin when attaching an event to its Deactivate link.
18967 *
18968 * @since 1.2.1.6 Always show the deactivation feedback form since we added automatic free version deactivation upon premium code activation.
18969 */
18970 $deactivate_link .= '<i class="fs-module-id" data-module-id="' . $this->_module_id . '"></i>';
18971
18972 // Append deactivation link.
18973 $before_deactivate['deactivate'] = $deactivate_link;
18974 }
18975
18976 return array_merge( $before_deactivate, $after_deactivate );
18977 }
18978
18979 /**
18980 * Adds admin message.
18981 *
18982 * @author Vova Feldman (@svovaf)
18983 * @since 1.0.4
18984 *
18985 * @param string $message
18986 * @param string $title
18987 * @param string $type
18988 */
18989 function add_admin_message( $message, $title = '', $type = 'success' ) {
18990 $this->_admin_notices->add( $message, $title, $type );
18991 }
18992
18993 /**
18994 * Adds sticky admin message.
18995 *
18996 * @author Vova Feldman (@svovaf)
18997 * @since 1.1.0
18998 *
18999 * @param string $message
19000 * @param string $id
19001 * @param string $title
19002 * @param string $type
19003 */
19004 function add_sticky_admin_message( $message, $id, $title = '', $type = 'success' ) {
19005 $this->_admin_notices->add_sticky( $message, $id, $title, $type );
19006 }
19007
19008 /**
19009 * Helper function that returns the final steps for the upgrade completion.
19010 *
19011 * If the module is already running the premium code, returns an empty string.
19012 *
19013 * @author Vova Feldman (@svovaf)
19014 * @since 1.2.1
19015 *
19016 * @param string $plan_title
19017 *
19018 * @return string
19019 */
19020 private function get_complete_upgrade_instructions( $plan_title = '' ) {
19021 $this->_logger->entrance();
19022
19023 $activate_license_string = $this->get_license_network_activation_notice();
19024
19025 if ( ! $this->has_premium_version() || $this->is_premium() ) {
19026 return '' . $activate_license_string;
19027 }
19028
19029 if ( empty( $plan_title ) ) {
19030 $plan_title = $this->get_plan_title();
19031 }
19032
19033 // @since 1.2.1.5 The free version is auto deactivated.
19034 $deactivation_step = version_compare( $this->version, '1.2.1.5', '<' ) ?
19035 ( '<li>' . $this->esc_html_inline( 'Deactivate the free version', 'deactivate-free-version' ) . '.</li>' ) :
19036 '';
19037
19038 return sprintf(
19039 ' %s: <ol><li>%s.</li>%s<li>%s (<a href="%s" target="_blank">%s</a>).</li></ol>',
19040 $this->get_text_inline( 'Please follow these steps to complete the upgrade', 'follow-steps-to-complete-upgrade' ),
19041 ( empty( $activate_license_string ) ? '' : $activate_license_string . '</li><li>' ) .
19042 $this->get_latest_download_link( sprintf(
19043 /* translators: %s: Plan title */
19044 $this->get_text_inline( 'Download the latest %s version', 'download-latest-x-version' ),
19045 $plan_title
19046 ) ),
19047 $deactivation_step,
19048 $this->get_text_inline( 'Upload and activate the downloaded version', 'upload-and-activate' ),
19049 '//bit.ly/upload-wp-' . $this->_module_type . 's',
19050 $this->get_text_inline( 'How to upload and activate?', 'howto-upload-activate' )
19051 );
19052 }
19053
19054 /**
19055 * This method is used to enrich the after upgrade notice instructions when the upgraded
19056 * license cannot be activated network wide (license quota isn't large enough).
19057 *
19058 * @author Vova Feldman (@svovaf)
19059 * @since 2.0.0
19060 *
19061 * @return string
19062 */
19063 private function get_license_network_activation_notice() {
19064 if ( ! $this->_is_network_active ) {
19065 // Module isn't network level activated.
19066 return '';
19067 }
19068
19069 if ( ! fs_is_network_admin() ) {
19070 // Not network level admin.
19071 return '';
19072 }
19073
19074 if ( get_blog_count() == 1 ) {
19075 // There's only a single site in the network so if there's a context license it was already activated.
19076 return '';
19077 }
19078
19079 if ( ! is_object( $this->_license ) ) {
19080 // No context license.
19081 return '';
19082 }
19083
19084 if ( $this->_license->is_single_site() && 0 < $this->_license->activated ) {
19085 // 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).
19086 return '';
19087 }
19088
19089 if ( $this->can_activate_license_on_network( $this->_license ) ) {
19090 // 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).
19091 return '';
19092 }
19093
19094 return sprintf(
19095 $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' ),
19096 '<a href="' . $this->get_account_url( false, array( 'activate_license' => 'true' ) ) . '">',
19097 '</a>'
19098 );
19099 }
19100
19101 /**
19102 * @author Vova Feldman (@svovaf)
19103 * @since 1.2.1.7
19104 *
19105 * @param string $key
19106 *
19107 * @return string
19108 */
19109 function get_text( $key ) {
19110 return fs_text( $key, $this->_slug );
19111 }
19112
19113 /**
19114 * @author Vova Feldman (@svovaf)
19115 * @since 1.2.3
19116 *
19117 * @param string $text Translatable string.
19118 * @param string $key String key for overrides.
19119 *
19120 * @return string
19121 */
19122 function get_text_inline( $text, $key = '' ) {
19123 return _fs_text_inline( $text, $key, $this->_slug );
19124 }
19125
19126 /**
19127 * @author Vova Feldman (@svovaf)
19128 * @since 1.2.3
19129 *
19130 * @param string $text Translatable string.
19131 * @param string $context Context information for the translators.
19132 * @param string $key String key for overrides.
19133 *
19134 * @return string
19135 */
19136 function get_text_x_inline( $text, $context, $key ) {
19137 return _fs_text_x_inline( $text, $context, $key, $this->_slug );
19138 }
19139
19140 /**
19141 * @author Vova Feldman (@svovaf)
19142 * @since 1.2.3
19143 *
19144 * @param string $text Translatable string.
19145 * @param string $key String key for overrides.
19146 *
19147 * @return string
19148 */
19149 function esc_html_inline( $text, $key ) {
19150 return esc_html( _fs_text_inline( $text, $key, $this->_slug ) );
19151 }
19152
19153 #----------------------------------------------------------------------------------
19154 #region Versioning
19155 #----------------------------------------------------------------------------------
19156
19157 /**
19158 * Check if Freemius in SDK upgrade mode.
19159 *
19160 * @author Vova Feldman (@svovaf)
19161 * @since 1.0.9
19162 *
19163 * @return bool
19164 */
19165 function is_sdk_upgrade_mode() {
19166 return isset( $this->_storage->sdk_upgrade_mode ) ?
19167 $this->_storage->sdk_upgrade_mode :
19168 false;
19169 }
19170
19171 /**
19172 * Turn SDK upgrade mode off.
19173 *
19174 * @author Vova Feldman (@svovaf)
19175 * @since 1.0.9
19176 */
19177 function set_sdk_upgrade_complete() {
19178 $this->_storage->sdk_upgrade_mode = false;
19179 }
19180
19181 /**
19182 * Check if plugin upgrade mode.
19183 *
19184 * @author Vova Feldman (@svovaf)
19185 * @since 1.0.9
19186 *
19187 * @return bool
19188 */
19189 function is_plugin_upgrade_mode() {
19190 return isset( $this->_storage->plugin_upgrade_mode ) ?
19191 $this->_storage->plugin_upgrade_mode :
19192 false;
19193 }
19194
19195 /**
19196 * Turn plugin upgrade mode off.
19197 *
19198 * @author Vova Feldman (@svovaf)
19199 * @since 1.0.9
19200 */
19201 function set_plugin_upgrade_complete() {
19202 $this->_storage->plugin_upgrade_mode = false;
19203 }
19204
19205 #endregion
19206
19207 #----------------------------------------------------------------------------------
19208 #region Permissions
19209 #----------------------------------------------------------------------------------
19210
19211 /**
19212 * Check if specific permission requested.
19213 *
19214 * @author Vova Feldman (@svovaf)
19215 * @since 1.1.6
19216 *
19217 * @param string $permission
19218 *
19219 * @return bool
19220 */
19221 function is_permission_requested( $permission ) {
19222 return isset( $this->_permissions[ $permission ] ) && ( true === $this->_permissions[ $permission ] );
19223 }
19224
19225 #endregion
19226
19227 #----------------------------------------------------------------------------------
19228 #region Auto Activation
19229 #----------------------------------------------------------------------------------
19230
19231 /**
19232 * Hints the SDK if running an auto-installation.
19233 *
19234 * @var bool
19235 */
19236 private $_isAutoInstall = false;
19237
19238 /**
19239 * After upgrade callback to install and auto activate a plugin.
19240 * This code will only be executed on explicit request from the user,
19241 * following the practice Jetpack are using with their theme installations.
19242 *
19243 * @link https://make.wordpress.org/plugins/2017/03/16/clarification-of-guideline-8-executable-code-and-installs/
19244 *
19245 * @author Vova Feldman (@svovaf)
19246 * @since 1.2.1.7
19247 */
19248 function _install_premium_version_ajax_action() {
19249 $this->_logger->entrance();
19250
19251 $this->check_ajax_referer( 'install_premium_version' );
19252
19253 if ( ! $this->is_registered() ) {
19254 // Not registered.
19255 self::shoot_ajax_failure( array(
19256 'message' => $this->get_text_inline( 'Auto installation only works for opted-in users.', 'auto-install-error-not-opted-in' ),
19257 'code' => 'premium_installed',
19258 ) );
19259 }
19260
19261 $plugin_id = fs_request_get( 'target_module_id', $this->get_id() );
19262
19263 if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
19264 // Invalid ID.
19265 self::shoot_ajax_failure( array(
19266 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
19267 'code' => 'invalid_module_id',
19268 ) );
19269 }
19270
19271 if ( $plugin_id == $this->get_id() ) {
19272 if ( $this->is_premium() ) {
19273 // Already using the premium code version.
19274 self::shoot_ajax_failure( array(
19275 'message' => $this->get_text_inline( 'Premium version already active.', 'auto-install-error-premium-activated' ),
19276 'code' => 'premium_installed',
19277 ) );
19278 }
19279 if ( ! $this->can_use_premium_code() ) {
19280 // Don't have access to the premium code.
19281 self::shoot_ajax_failure( array(
19282 'message' => $this->get_text_inline( 'You do not have a valid license to access the premium version.', 'auto-install-error-invalid-license' ),
19283 'code' => 'invalid_license',
19284 ) );
19285 }
19286 if ( ! $this->has_release_on_freemius() ) {
19287 // Plugin is a serviceware, no premium code version.
19288 self::shoot_ajax_failure( array(
19289 'message' => $this->get_text_inline( 'Plugin is a "Serviceware" which means it does not have a premium code version.', 'auto-install-error-serviceware' ),
19290 'code' => 'premium_version_missing',
19291 ) );
19292 }
19293 } else {
19294 $addon = $this->get_addon( $plugin_id );
19295
19296 if ( ! is_object( $addon ) ) {
19297 // Invalid add-on ID.
19298 self::shoot_ajax_failure( array(
19299 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
19300 'code' => 'invalid_module_id',
19301 ) );
19302 }
19303
19304 if ( $this->is_addon_activated( $plugin_id, true ) ) {
19305 // Premium add-on version is already activated.
19306 self::shoot_ajax_failure( array(
19307 'message' => $this->get_text_inline( 'Premium add-on version already installed.', 'auto-install-error-premium-addon-activated' ),
19308 'code' => 'premium_installed',
19309 ) );
19310 }
19311 }
19312
19313 $this->_isAutoInstall = true;
19314
19315 // Try to install and activate.
19316 $updater = FS_Plugin_Updater::instance( $this );
19317 $result = $updater->install_and_activate_plugin( $plugin_id );
19318
19319 if ( is_array( $result ) && ! empty( $result['message'] ) ) {
19320 self::shoot_ajax_failure( array(
19321 'message' => $result['message'],
19322 'code' => $result['code'],
19323 ) );
19324 }
19325
19326 self::shoot_ajax_success( $result );
19327 }
19328
19329 /**
19330 * Displays module activation dialog box after a successful upgrade
19331 * where the user explicitly requested to auto download and install
19332 * the premium version.
19333 *
19334 * @author Vova Feldman (@svovaf)
19335 * @since 1.2.1.7
19336 */
19337 function _add_auto_installation_dialog_box() {
19338 $this->_logger->entrance();
19339
19340 if ( ! $this->is_registered() ) {
19341 // Not registered.
19342 return;
19343 }
19344
19345 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
19346
19347 if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
19348 // Invalid module ID.
19349 return;
19350 }
19351
19352 if ( $plugin_id == $this->get_id() ) {
19353 if ( $this->is_premium() ) {
19354 // Already using the premium code version.
19355 return;
19356 }
19357 if ( ! $this->can_use_premium_code() ) {
19358 // Don't have access to the premium code.
19359 return;
19360 }
19361 if ( ! $this->has_release_on_freemius() ) {
19362 // Plugin is a serviceware, no premium code version.
19363 return;
19364 }
19365 } else {
19366 $addon = $this->get_addon( $plugin_id );
19367
19368 if ( ! is_object( $addon ) ) {
19369 // Invalid add-on ID.
19370 return;
19371 }
19372
19373 if ( $this->is_addon_activated( $plugin_id, true ) ) {
19374 // Premium add-on version is already activated.
19375 return;
19376 }
19377 }
19378
19379 $vars = array(
19380 'id' => $this->_module_id,
19381 'target_module_id' => $plugin_id,
19382 'slug' => $this->_slug,
19383 );
19384
19385 fs_require_template( 'auto-installation.php', $vars );
19386 }
19387
19388 #endregion
19389
19390 #--------------------------------------------------------------------------------
19391 #region Tabs Integration
19392 #--------------------------------------------------------------------------------
19393
19394 #region Module's Original Tabs
19395
19396 /**
19397 * Inject a JavaScript logic to capture the theme tabs HTML.
19398 *
19399 * @author Vova Feldman (@svovaf)
19400 * @since 1.2.2.7
19401 */
19402 function _tabs_capture() {
19403 $this->_logger->entrance();
19404
19405 if ( ! $this->is_theme_settings_page() ||
19406 ! $this->is_matching_url( $this->main_menu_url() )
19407 ) {
19408 return;
19409 }
19410
19411 $params = array(
19412 'id' => $this->_module_id,
19413 );
19414
19415 fs_require_once_template( 'tabs-capture-js.php', $params );
19416 }
19417
19418 /**
19419 * Cache theme's tabs HTML for a week. The cache will also be set as expired
19420 * after version and type (free/premium) changes, in addition to the week period.
19421 *
19422 * @author Vova Feldman (@svovaf)
19423 * @since 1.2.2.7
19424 */
19425 function _store_tabs_ajax_action() {
19426 $this->_logger->entrance();
19427
19428 $this->check_ajax_referer( 'store_tabs' );
19429
19430 // Init filesystem if not yet initiated.
19431 WP_Filesystem();
19432
19433 // Get POST body HTML data.
19434 global $wp_filesystem;
19435 $tabs_html = $wp_filesystem->get_contents( "php://input" );
19436
19437 if ( is_string( $tabs_html ) ) {
19438 $tabs_html = trim( $tabs_html );
19439 }
19440
19441 if ( ! is_string( $tabs_html ) || empty( $tabs_html ) ) {
19442 self::shoot_ajax_failure();
19443 }
19444
19445 $this->_cache->set( 'tabs', $tabs_html, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
19446
19447 self::shoot_ajax_success();
19448 }
19449
19450 /**
19451 * Cache theme's settings page custom styles. The cache will also be set as expired
19452 * after version and type (free/premium) changes, in addition to the week period.
19453 *
19454 * @author Vova Feldman (@svovaf)
19455 * @since 1.2.2.7
19456 */
19457 function _store_tabs_styles() {
19458 $this->_logger->entrance();
19459
19460 if ( ! $this->is_theme_settings_page() ||
19461 ! $this->is_matching_url( $this->main_menu_url() )
19462 ) {
19463 return;
19464 }
19465
19466 $wp_styles = wp_styles();
19467
19468 $theme_styles_url = get_template_directory_uri();
19469
19470 $stylesheets = array();
19471 foreach ( $wp_styles->queue as $handler ) {
19472 if ( fs_starts_with( $handler, 'fs_' ) ) {
19473 // Assume that stylesheets that their handler starts with "fs_" belong to the SDK.
19474 continue;
19475 }
19476
19477 /**
19478 * @var _WP_Dependency $stylesheet
19479 */
19480 $stylesheet = $wp_styles->registered[ $handler ];
19481
19482 if ( fs_starts_with( $stylesheet->src, $theme_styles_url ) ) {
19483 $stylesheets[] = $stylesheet->src;
19484 }
19485 }
19486
19487 if ( ! empty( $stylesheets ) ) {
19488 $this->_cache->set( 'tabs_stylesheets', $stylesheets, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
19489 }
19490 }
19491
19492 /**
19493 * Check if module's original settings page has any tabs.
19494 *
19495 * @author Vova Feldman (@svovaf)
19496 * @since 1.2.2.7
19497 *
19498 * @return bool
19499 */
19500 private function has_tabs() {
19501 return $this->_cache->has( 'tabs' );
19502 }
19503
19504 /**
19505 * Get module's settings page HTML content, starting
19506 * from the beginning of the <div class="wrap"> element,
19507 * until the tabs HTML (including).
19508 *
19509 * @author Vova Feldman (@svovaf)
19510 * @since 1.2.2.7
19511 *
19512 * @return string
19513 */
19514 private function get_tabs_html() {
19515 $this->_logger->entrance();
19516
19517 return $this->_cache->get( 'tabs' );
19518 }
19519
19520 /**
19521 * Check if page should include tabs.
19522 *
19523 * @author Vova Feldman (@svovaf)
19524 * @since 1.2.2.7
19525 *
19526 * @return bool
19527 */
19528 private function should_page_include_tabs() {
19529 if ( ! $this->has_settings_menu() ) {
19530 // Don't add tabs if no settings at all.
19531 return false;
19532 }
19533
19534 if ( ! $this->is_theme() ) {
19535 // Only add tabs to themes for now.
19536 return false;
19537 }
19538
19539 if ( ! $this->has_paid_plan() && ! $this->has_addons() ) {
19540 // Only add tabs to monetizing themes.
19541 return false;
19542 }
19543
19544 if ( ! $this->is_theme_settings_page() ) {
19545 // Only add tabs if browsing one of the theme's setting pages.
19546 return false;
19547 }
19548
19549 if ( $this->is_admin_page( 'pricing' ) && fs_request_get_bool( 'checkout' ) ) {
19550 // Don't add tabs on checkout page, we want to reduce distractions
19551 // as much as possible.
19552 return false;
19553 }
19554
19555 return true;
19556 }
19557
19558 /**
19559 * Add the tabs HTML before the setting's page content and
19560 * enqueue any required stylesheets.
19561 *
19562 * @author Vova Feldman (@svovaf)
19563 * @since 1.2.2.7
19564 *
19565 * @return bool If tabs were included.
19566 */
19567 function _add_tabs_before_content() {
19568 $this->_logger->entrance();
19569
19570 if ( ! $this->should_page_include_tabs() ) {
19571 return false;
19572 }
19573
19574 /**
19575 * Enqueue the original stylesheets that are included in the
19576 * theme settings page. That way, if the theme settings has
19577 * some custom _styled_ content above the tabs UI, this
19578 * will make sure that the styling is preserved.
19579 */
19580 $stylesheets = $this->_cache->get( 'tabs_stylesheets', array() );
19581 if ( is_array( $stylesheets ) ) {
19582 for ( $i = 0, $len = count( $stylesheets ); $i < $len; $i ++ ) {
19583 wp_enqueue_style( "fs_{$this->_module_id}_tabs_{$i}", $stylesheets[ $i ] );
19584 }
19585 }
19586
19587 // Cut closing </div> tag.
19588 echo substr( trim( $this->get_tabs_html() ), 0, - 6 );
19589
19590 return true;
19591 }
19592
19593 /**
19594 * Add the tabs closing HTML after the setting's page content.
19595 *
19596 * @author Vova Feldman (@svovaf)
19597 * @since 1.2.2.7
19598 *
19599 * @return bool If tabs closing HTML was included.
19600 */
19601 function _add_tabs_after_content() {
19602 $this->_logger->entrance();
19603
19604 if ( ! $this->should_page_include_tabs() ) {
19605 return false;
19606 }
19607
19608 echo '</div>';
19609
19610 return true;
19611 }
19612
19613 #endregion
19614
19615 /**
19616 * Add in-page JavaScript to inject the Freemius tabs into
19617 * the module's setting tabs section.
19618 *
19619 * @author Vova Feldman (@svovaf)
19620 * @since 1.2.2.7
19621 */
19622 function _add_freemius_tabs() {
19623 $this->_logger->entrance();
19624
19625 if ( ! $this->should_page_include_tabs() ) {
19626 return;
19627 }
19628
19629 $params = array( 'id' => $this->_module_id );
19630 fs_require_once_template( 'tabs.php', $params );
19631 }
19632
19633 #endregion
19634
19635 #--------------------------------------------------------------------------------
19636 #region Customizer Integration for Themes
19637 #--------------------------------------------------------------------------------
19638
19639 /**
19640 * @author Vova Feldman (@svovaf)
19641 * @since 1.2.2.7
19642 *
19643 * @param WP_Customize_Manager $customizer
19644 */
19645 function _customizer_register( $customizer ) {
19646 $this->_logger->entrance();
19647
19648 if ( $this->is_pricing_page_visible() ) {
19649 require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-upsell-control.php';
19650
19651 $customizer->add_section( 'freemius_upsell', array(
19652 'title' => '&#9733; ' . $this->get_text_inline( 'View paid features', 'view-paid-features' ),
19653 'priority' => 1,
19654 ) );
19655 $customizer->add_setting( 'freemius_upsell', array(
19656 'sanitize_callback' => 'esc_html',
19657 ) );
19658
19659 $customizer->add_control( new FS_Customizer_Upsell_Control( $customizer, 'freemius_upsell', array(
19660 'fs' => $this,
19661 'section' => 'freemius_upsell',
19662 'priority' => 100,
19663 ) ) );
19664 }
19665
19666 if ( $this->is_page_visible( 'contact' ) || $this->is_page_visible( 'support' ) ) {
19667 require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-support-section.php';
19668
19669 // Main Documentation Link In Customizer Root.
19670 $customizer->add_section( new FS_Customizer_Support_Section( $customizer, 'freemius_support', array(
19671 'fs' => $this,
19672 'priority' => 1000,
19673 ) ) );
19674 }
19675 }
19676
19677 #endregion
19678
19679 /**
19680 * If the theme has a paid version, add some custom
19681 * styling to the theme's premium version (if exists)
19682 * to highlight that it's the premium version of the
19683 * same theme, making it easier for identification
19684 * after the user upgrades and upload it to the site.
19685 *
19686 * @author Vova Feldman (@svovaf)
19687 * @since 1.2.2.7
19688 */
19689 function _style_premium_theme() {
19690 $this->_logger->entrance();
19691
19692 if ( ! self::is_themes_page() ) {
19693 // Only include in the themes page.
19694 return;
19695 }
19696
19697 if ( ! $this->has_paid_plan() ) {
19698 // Only include if has any paid plans.
19699 return;
19700 }
19701
19702 $params = null;
19703 fs_require_once_template( '/js/jquery.content-change.php', $params );
19704
19705 $params = array(
19706 'slug' => $this->_slug,
19707 'id' => $this->_module_id,
19708 );
19709
19710 fs_require_template( '/js/style-premium-theme.php', $params );
19711 }
19712
19713 /**
19714 * This method will return the absolute URL of the module's local icon.
19715 *
19716 * When you are running your plugin or theme on a **localhost** environment, if the icon
19717 * is not found in the local assets folder, try to fetch the icon URL from Freemius. If not set and
19718 * it's a plugin hosted on WordPress.org, try fetching the icon URL from wordpress.org.
19719 * If an icon is found, this method will automatically attempt to download the icon and store it
19720 * in /freemius/assets/img/{slug}.{png|jpg|gif|svg}.
19721 *
19722 * It's important to mention that this method is NOT phoning home since the developer will deploy
19723 * the product with the local icon in the assets folder. The download process just simplifies
19724 * the process for the developer.
19725 *
19726 * @author Vova Feldman (@svovaf)
19727 * @since 2.0.0
19728 *
19729 * @return string
19730 */
19731 function get_local_icon_url() {
19732 global $fs_active_plugins;
19733
19734 /**
19735 * @since 1.1.7.5
19736 */
19737 $local_path = $this->apply_filters( 'plugin_icon', false );
19738
19739 if ( is_string( $local_path ) ) {
19740 $icons = array( $local_path );
19741 } else {
19742 $img_dir = WP_FS__DIR_IMG;
19743
19744 // Locate the main assets folder.
19745 if ( 1 < count( $fs_active_plugins->plugins ) ) {
19746 $plugin_or_theme_img_dir = ( $this->is_plugin() ? WP_PLUGIN_DIR : get_theme_root() );
19747
19748 foreach ( $fs_active_plugins->plugins as $sdk_path => &$data ) {
19749 if ( $data->plugin_path == $this->get_plugin_basename() ) {
19750 $img_dir = $plugin_or_theme_img_dir
19751 . '/'
19752 . str_replace( '../themes/', '', $sdk_path )
19753 . '/assets/img';
19754
19755 break;
19756 }
19757 }
19758 }
19759
19760 // Try to locate the icon in the assets folder.
19761 $icons = glob( fs_normalize_path( $img_dir . "/{$this->_slug}.*" ) );
19762
19763 if ( ! is_array( $icons ) || 0 === count( $icons ) ) {
19764 if ( ! WP_FS__IS_LOCALHOST && $this->is_theme() ) {
19765 $icons = array(
19766 fs_normalize_path( $img_dir . '/theme-icon.png' )
19767 );
19768 } else {
19769 $icon_found = false;
19770 $local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.png" );
19771
19772 $have_write_permissions = ( 'direct' === get_filesystem_method( array(), fs_normalize_path( $img_dir ) ) );
19773
19774 /**
19775 * IMPORTANT: THIS CODE WILL NEVER RUN AFTER THE PLUGIN IS IN THE REPO.
19776 *
19777 * This code will only be executed once during the testing
19778 * of the plugin in a local environment. The plugin icon file WILL
19779 * already exist in the assets folder when the plugin is deployed to
19780 * the repository.
19781 */
19782 if ( WP_FS__IS_LOCALHOST && $have_write_permissions ) {
19783 // Fetch icon from Freemius.
19784 $icon = $this->fetch_remote_icon_url();
19785
19786 // Fetch icon from WordPress.org.
19787 if ( empty( $icon ) && $this->is_plugin() && $this->is_org_repo_compliant() ) {
19788 if ( ! function_exists( 'plugins_api' ) ) {
19789 require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
19790 }
19791
19792 $plugin_information = plugins_api( 'plugin_information', array(
19793 'slug' => $this->_slug,
19794 'fields' => array(
19795 'sections' => false,
19796 'tags' => false,
19797 'icons' => true
19798 )
19799 ) );
19800
19801 if (
19802 ! is_wp_error( $plugin_information )
19803 && isset( $plugin_information->icons )
19804 && ! empty( $plugin_information->icons )
19805 ) {
19806 /**
19807 * Get the smallest icon.
19808 *
19809 * @author Leo Fajardo (@leorw)
19810 * @since 1.2.2
19811 */
19812 $icon = end( $plugin_information->icons );
19813 }
19814 }
19815
19816 if ( ! empty( $icon ) ) {
19817 if ( 0 !== strpos( $icon, 'http' ) ) {
19818 $icon = 'http:' . $icon;
19819 }
19820
19821 /**
19822 * Get a clean file extension, e.g.: "jpg" and not "jpg?rev=1305765".
19823 *
19824 * @author Leo Fajardo (@leorw)
19825 * @since 1.2.2
19826 */
19827 $ext = pathinfo( strtok( $icon, '?' ), PATHINFO_EXTENSION );
19828
19829 $local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.{$ext}" );
19830
19831 // Try to download the icon.
19832 $icon_found = fs_download_image( $icon, $local_path );
19833 }
19834 }
19835
19836 if ( ! $icon_found ) {
19837 // No icons found, fallback to default icon.
19838 if ( $have_write_permissions ) {
19839 // If have write permissions, copy default icon.
19840 copy( fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" ), $local_path );
19841 } else {
19842 // If doesn't have write permissions, use default icon path.
19843 $local_path = fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" );
19844 }
19845 }
19846
19847 $icons = array( $local_path );
19848 }
19849 }
19850 }
19851
19852 $icon_dir = dirname( $icons[0] );
19853
19854 return fs_img_url( substr( $icons[0], strlen( $icon_dir ) ), $icon_dir );
19855 }
19856
19857 /**
19858 * Fetch module's extended info.
19859 *
19860 * @author Vova Feldman (@svovaf)
19861 * @since 2.0.0
19862 *
19863 * @return object|mixed
19864 */
19865 private function fetch_module_info() {
19866 return $this->get_api_plugin_scope()->get( 'info.json', false, WP_FS__TIME_WEEK_IN_SEC );
19867 }
19868
19869 /**
19870 * Fetch module's remote icon URL.
19871 *
19872 * @author Vova Feldman (@svovaf)
19873 * @since 2.0.0
19874 *
19875 * @return string
19876 */
19877 function fetch_remote_icon_url() {
19878 $info = $this->fetch_module_info();
19879
19880 return ( $this->is_api_result_object( $info, 'icon' ) && is_string( $info->icon ) ) ?
19881 $info->icon :
19882 '';
19883 }
19884
19885 #----------------------------------------------------------------------------------
19886 #region Marketing
19887 #----------------------------------------------------------------------------------
19888
19889 /**
19890 * Check if current user purchased any other plugins before.
19891 *
19892 * @author Vova Feldman (@svovaf)
19893 * @since 1.0.9
19894 *
19895 * @return bool
19896 */
19897 function has_purchased_before() {
19898 // TODO: Implement has_purchased_before() method.
19899 throw new Exception( 'not implemented' );
19900 }
19901
19902 /**
19903 * Check if current user classified as an agency.
19904 *
19905 * @author Vova Feldman (@svovaf)
19906 * @since 1.0.9
19907 *
19908 * @return bool
19909 */
19910 function is_agency() {
19911 // TODO: Implement is_agency() method.
19912 throw new Exception( 'not implemented' );
19913 }
19914
19915 /**
19916 * Check if current user classified as a developer.
19917 *
19918 * @author Vova Feldman (@svovaf)
19919 * @since 1.0.9
19920 *
19921 * @return bool
19922 */
19923 function is_developer() {
19924 // TODO: Implement is_developer() method.
19925 throw new Exception( 'not implemented' );
19926 }
19927
19928 /**
19929 * Check if current user classified as a business.
19930 *
19931 * @author Vova Feldman (@svovaf)
19932 * @since 1.0.9
19933 *
19934 * @return bool
19935 */
19936 function is_business() {
19937 // TODO: Implement is_business() method.
19938 throw new Exception( 'not implemented' );
19939 }
19940
19941 #endregion
19942 }