PluginProbe
Buttonizer – Floating Menus, Sticky Buttons, & Popup Builder / 1.4.1
Buttonizer – Floating Menus, Sticky Buttons, & Popup Builder v1.4.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.4.1, at freemius/includes/class-freemius.php

14,324 lines 413.8 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 * @var bool Hints the SDK if plugin can support anonymous mode (if skip connect is visible).
102 */
103 private $_enable_anonymous;
104
105 /**
106 * @since 1.1.7.5
107 * @var bool Hints the SDK if plugin should run in anonymous mode (only adds feedback form).
108 */
109 private $_anonymous_mode;
110
111 /**
112 * @since 1.1.9
113 * @var bool Hints the SDK if plugin have any free plans.
114 */
115 private $_is_premium_only;
116
117 /**
118 * @since 1.2.1.6
119 * @var bool Hints the SDK if plugin have premium code version at all.
120 */
121 private $_has_premium_version;
122
123 /**
124 * @since 1.2.1.6
125 * @var bool Hints the SDK if plugin should ignore pending mode by simulating a skip.
126 */
127 private $_ignore_pending_mode;
128
129 /**
130 * @since 1.0.8
131 * @var bool Hints the SDK if the plugin has any paid plans.
132 */
133 private $_has_paid_plans;
134
135 /**
136 * @since 1.2.1.5
137 * @var int Hints the SDK if the plugin offers a trial period. If negative, no trial, if zero - has a trial but
138 * without a specified period, if positive - the number of trial days.
139 */
140 private $_trial_days = - 1;
141
142 /**
143 * @since 1.2.1.5
144 * @var bool Hints the SDK if the trial requires a payment method or not.
145 */
146 private $_is_trial_require_payment = false;
147
148 /**
149 * @since 1.0.7
150 * @var bool Hints the SDK if the plugin is WordPress.org compliant.
151 */
152 private $_is_org_compliant;
153
154 /**
155 * @since 1.0.7
156 * @var bool Hints the SDK if the plugin is has add-ons.
157 */
158 private $_has_addons;
159
160 /**
161 * @since 1.1.6
162 * @var string[]bool.
163 */
164 private $_permissions;
165
166 /**
167 * @var FS_Key_Value_Storage
168 */
169 private $_storage;
170
171 /**
172 * @since 1.2.2.7
173 * @var FS_Cache_Manager
174 */
175 private $_cache;
176
177 /**
178 * @since 1.0.0
179 *
180 * @var FS_Logger
181 */
182 private $_logger;
183 /**
184 * @since 1.0.4
185 *
186 * @var FS_Plugin
187 */
188 private $_plugin = false;
189 /**
190 * @since 1.0.4
191 *
192 * @var FS_Plugin|false
193 */
194 private $_parent_plugin = false;
195 /**
196 * @since 1.1.1
197 *
198 * @var Freemius
199 */
200 private $_parent = false;
201 /**
202 * @since 1.0.1
203 *
204 * @var FS_User
205 */
206 private $_user = false;
207 /**
208 * @since 1.0.1
209 *
210 * @var FS_Site
211 */
212 private $_site = false;
213 /**
214 * @since 1.0.1
215 *
216 * @var FS_Plugin_License
217 */
218 private $_license;
219 /**
220 * @since 1.0.2
221 *
222 * @var FS_Plugin_Plan[]
223 */
224 private $_plans = false;
225 /**
226 * @var FS_Plugin_License[]
227 * @since 1.0.5
228 */
229 private $_licenses = false;
230
231 /**
232 * @since 1.0.1
233 *
234 * @var FS_Admin_Menu_Manager
235 */
236 private $_menu;
237
238 /**
239 * @var FS_Admin_Notice_Manager
240 */
241 private $_admin_notices;
242
243 /**
244 * @since 1.1.6
245 *
246 * @var FS_Admin_Notice_Manager
247 */
248 private static $_global_admin_notices;
249
250 /**
251 * @var FS_Logger
252 * @since 1.0.0
253 */
254 private static $_static_logger;
255
256 /**
257 * @var FS_Option_Manager
258 * @since 1.0.2
259 */
260 private static $_accounts;
261
262 /**
263 * @since 1.2.2
264 *
265 * @var number
266 */
267 private $_module_id;
268
269 /**
270 * @var Freemius[]
271 */
272 private static $_instances = array();
273
274 /**
275 * @author Leo Fajardo (@leorw)
276 *
277 * @since 1.2.3
278 *
279 * @var FS_Affiliate
280 */
281 private $affiliate = null;
282
283 /**
284 * @author Leo Fajardo (@leorw)
285 *
286 * @since 1.2.3
287 *
288 * @var FS_AffiliateTerms
289 */
290 private $plugin_affiliate_terms = null;
291
292 /**
293 * @author Leo Fajardo (@leorw)
294 *
295 * @since 1.2.3
296 *
297 * @var FS_AffiliateTerms
298 */
299 private $custom_affiliate_terms = null;
300
301 #region Uninstall Reasons IDs
302
303 const REASON_NO_LONGER_NEEDED = 1;
304 const REASON_FOUND_A_BETTER_PLUGIN = 2;
305 const REASON_NEEDED_FOR_A_SHORT_PERIOD = 3;
306 const REASON_BROKE_MY_SITE = 4;
307 const REASON_SUDDENLY_STOPPED_WORKING = 5;
308 const REASON_CANT_PAY_ANYMORE = 6;
309 const REASON_OTHER = 7;
310 const REASON_DIDNT_WORK = 8;
311 const REASON_DONT_LIKE_TO_SHARE_MY_INFORMATION = 9;
312 const REASON_COULDNT_MAKE_IT_WORK = 10;
313 const REASON_GREAT_BUT_NEED_SPECIFIC_FEATURE = 11;
314 const REASON_NOT_WORKING = 12;
315 const REASON_NOT_WHAT_I_WAS_LOOKING_FOR = 13;
316 const REASON_DIDNT_WORK_AS_EXPECTED = 14;
317 const REASON_TEMPORARY_DEACTIVATION = 15;
318
319 #endregion
320
321 /* Ctor
322 ------------------------------------------------------------------------------------------------------------------*/
323
324 /**
325 * Main singleton instance.
326 *
327 * @author Vova Feldman (@svovaf)
328 * @since 1.0.0
329 *
330 * @param number $module_id
331 * @param string|bool $slug
332 * @param bool $is_init Since 1.2.1 Is initiation sequence.
333 */
334 private function __construct( $module_id, $slug = false, $is_init = false ) {
335 if ( $is_init && is_numeric( $module_id ) && is_string( $slug ) ) {
336 $this->store_id_slug_type_path_map( $module_id, $slug );
337 }
338
339 $this->_module_id = $module_id;
340 $this->_slug = $this->get_slug();
341 $this->_module_type = $this->get_module_type();
342
343 $this->_storage = FS_Key_Value_Storage::instance( $this->_module_type . '_data', $this->_slug );
344 $this->_cache = FS_Cache_Manager::get_manager( WP_FS___OPTION_PREFIX . "cache_{$module_id}" );
345
346 $this->_logger = FS_Logger::get_logger( WP_FS__SLUG . '_' . $this->get_unique_affix(), WP_FS__DEBUG_SDK, WP_FS__ECHO_DEBUG_SDK );
347
348 $this->_plugin_main_file_path = $this->_find_caller_plugin_file( $is_init );
349 $this->_plugin_dir_path = plugin_dir_path( $this->_plugin_main_file_path );
350 $this->_plugin_basename = $this->get_plugin_basename();
351 $this->_free_plugin_basename = str_replace( '-premium/', '/', $this->_plugin_basename );
352
353 $base_name_split = explode( '/', $this->_plugin_basename );
354 $this->_plugin_dir_name = $base_name_split[0];
355
356 if ( $this->_logger->is_on() ) {
357 $this->_logger->info( 'plugin_main_file_path = ' . $this->_plugin_main_file_path );
358 $this->_logger->info( 'plugin_dir_path = ' . $this->_plugin_dir_path );
359 $this->_logger->info( 'plugin_basename = ' . $this->_plugin_basename );
360 $this->_logger->info( 'free_plugin_basename = ' . $this->_free_plugin_basename );
361 $this->_logger->info( 'plugin_dir_name = ' . $this->_plugin_dir_name );
362 }
363
364 // Remember link between file to slug.
365 $this->store_file_slug_map();
366
367 // Store plugin's initial install timestamp.
368 if ( ! isset( $this->_storage->install_timestamp ) ) {
369 $this->_storage->install_timestamp = WP_FS__SCRIPT_START_TIME;
370 }
371
372 if ( ! is_object( $this->_plugin ) ) {
373 $this->_plugin = FS_Plugin_Manager::instance( $this->_module_id )->get();
374 }
375
376 $this->_admin_notices = FS_Admin_Notice_Manager::instance(
377 $this->_slug . ( $this->is_theme() ? ':theme' : '' ),
378 /**
379 * Ensure that the admin notice will always have a title by using the stored plugin title if available and
380 * retrieving the title via the "get_plugin_name" method if there is no stored plugin title available.
381 *
382 * @author Leo Fajardo (@leorw)
383 * @since 1.2.2
384 */
385 ( is_object( $this->_plugin ) ? $this->_plugin->title : $this->get_plugin_name() ),
386 $this->get_unique_affix()
387 );
388
389 if ( 'true' === fs_request_get( 'fs_clear_api_cache' ) ||
390 'true' === fs_request_is_action( 'restart_freemius' )
391 ) {
392 FS_Api::clear_cache();
393 $this->_cache->clear();
394 }
395
396 $this->_register_hooks();
397
398 $this->_load_account();
399
400 $this->_version_updates_handler();
401 }
402
403 /**
404 * Checks whether this module has a settings menu.
405 *
406 * @author Leo Fajardo (@leorw)
407 * @since 1.2.2
408 *
409 * @return bool
410 */
411 function has_settings_menu() {
412 return $this->_menu->has_menu();
413 }
414
415 /**
416 * Check if the context module is free wp.org theme.
417 *
418 * This method is helpful because:
419 * 1. wp.org themes are limited to a single submenu item,
420 * and sub-submenu items are most likely not allowed (never verified).
421 * 2. wp.org themes are not allowed to redirect the user
422 * after the theme activation, therefore, the agreed UX
423 * is showing the opt-in as a modal dialog box after
424 * activation (approved by @otto42, @emiluzelac, @greenshady, @grapplerulrich).
425 *
426 * @author Vova Feldman (@svovaf)
427 * @since 1.2.2.7
428 *
429 * @return bool
430 */
431 function is_free_wp_org_theme() {
432 return (
433 $this->is_theme() &&
434 $this->is_org_repo_compliant() &&
435 ! $this->is_premium()
436 );
437 }
438
439 /**
440 * Checks whether this a submenu item is visible.
441 *
442 * @author Vova Feldman (@svovaf)
443 * @since 1.2.2.6
444 * @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.
445 *
446 * @param string $slug
447 *
448 * @return bool
449 */
450 function is_submenu_item_visible( $slug ) {
451 if ( $this->is_admin_page( $slug ) ) {
452 /**
453 * It is the current context page, so show the submenu item
454 * so the user will have the right context page, even if it
455 * was set to hidden.
456 */
457 return true;
458 }
459
460 if ( ! $this->has_settings_menu() ) {
461 // No menu settings at all.
462 return false;
463 }
464
465 if ( $this->is_free_wp_org_theme() ) {
466 /**
467 * wp.org themes are limited to a single submenu item, and
468 * sub-submenu items are most likely not allowed (never verified).
469 */
470 return false;
471 }
472
473 return $this->_menu->is_submenu_item_visible( $slug );
474 }
475
476 /**
477 * Check if a Freemius page should be accessible via the UI.
478 *
479 * @author Vova Feldman (@svovaf)
480 * @since 1.2.2.7
481 *
482 * @param string $slug
483 *
484 * @return bool
485 */
486 function is_page_visible( $slug ) {
487 if ( $this->is_admin_page( $slug ) ) {
488 return true;
489 }
490
491 return $this->_menu->is_submenu_item_visible( $slug, true, true );
492 }
493
494 /**
495 * @author Vova Feldman (@svovaf)
496 * @since 1.0.9
497 */
498 private function _version_updates_handler() {
499 if ( ! isset( $this->_storage->sdk_version ) || $this->_storage->sdk_version != $this->version ) {
500 // Freemius version upgrade mode.
501 $this->_storage->sdk_last_version = $this->_storage->sdk_version;
502 $this->_storage->sdk_version = $this->version;
503
504 if ( empty( $this->_storage->sdk_last_version ) ||
505 version_compare( $this->_storage->sdk_last_version, $this->version, '<' )
506 ) {
507 $this->_storage->sdk_upgrade_mode = true;
508 $this->_storage->sdk_downgrade_mode = false;
509 } else {
510 $this->_storage->sdk_downgrade_mode = true;
511 $this->_storage->sdk_upgrade_mode = false;
512
513 }
514
515 $this->do_action( 'sdk_version_update', $this->_storage->sdk_last_version, $this->version );
516 }
517
518 $plugin_version = $this->get_plugin_version();
519 if ( ! isset( $this->_storage->plugin_version ) || $this->_storage->plugin_version != $plugin_version ) {
520 // Plugin version upgrade mode.
521 $this->_storage->plugin_last_version = $this->_storage->plugin_version;
522 $this->_storage->plugin_version = $plugin_version;
523
524 if ( empty( $this->_storage->plugin_last_version ) ||
525 version_compare( $this->_storage->plugin_last_version, $plugin_version, '<' )
526 ) {
527 $this->_storage->plugin_upgrade_mode = true;
528 $this->_storage->plugin_downgrade_mode = false;
529 } else {
530 $this->_storage->plugin_downgrade_mode = true;
531 $this->_storage->plugin_upgrade_mode = false;
532 }
533
534 if ( ! empty( $this->_storage->plugin_last_version ) ) {
535 // Different version of the plugin was installed before, therefore it's an update.
536 $this->_storage->is_plugin_new_install = false;
537 }
538
539 $this->do_action( 'plugin_version_update', $this->_storage->plugin_last_version, $plugin_version );
540 }
541 }
542
543 /**
544 * @author Vova Feldman (@svovaf)
545 * @since 1.1.5
546 *
547 * @param string $sdk_prev_version
548 * @param string $sdk_version
549 */
550 function _data_migration( $sdk_prev_version, $sdk_version ) {
551 /**
552 * @since 1.1.7.3 Fixed unwanted connectivity test cleanup.
553 */
554 if ( empty( $sdk_prev_version ) ) {
555 return;
556 }
557
558 if ( version_compare( $sdk_prev_version, '1.2.3', '<' ) &&
559 version_compare( $sdk_version, '1.2.3', '>=' )
560 ) {
561 /**
562 * Starting from version 1.2.3, paths are stored as relative paths and not absolute paths; so when
563 * upgrading to 1.2.3, make paths relative.
564 *
565 * @author Leo Fajardo (@leorw)
566 */
567 $this->make_paths_relative();
568 }
569
570 if ( version_compare( $sdk_prev_version, '1.1.5', '<' ) &&
571 version_compare( $sdk_version, '1.1.5', '>=' )
572 ) {
573 // On version 1.1.5 merged connectivity and is_on data.
574 if ( isset( $this->_storage->connectivity_test ) ) {
575 if ( ! isset( $this->_storage->is_on ) ) {
576 unset( $this->_storage->connectivity_test );
577 } else {
578 $connectivity_data = $this->_storage->connectivity_test;
579 $connectivity_data['is_active'] = $this->_storage->is_on['is_active'];
580 $connectivity_data['timestamp'] = $this->_storage->is_on['timestamp'];
581
582 // Override.
583 $this->_storage->connectivity_test = $connectivity_data;
584
585 // Remove previous structure.
586 unset( $this->_storage->is_on );
587 }
588
589 }
590 }
591 }
592
593 /**
594 * Makes paths relative.
595 *
596 * @author Leo Fajardo
597 * @since 1.2.3
598 */
599 private function make_paths_relative() {
600 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
601
602 if ( isset( $id_slug_type_path_map[ $this->_module_id ]['path'] ) ) {
603 $id_slug_type_path_map[ $this->_module_id ]['path'] = $this->get_relative_path( $id_slug_type_path_map[ $this->_module_id ]['path'] );
604
605 self::$_accounts->set_option( 'id_slug_type_path_map', $id_slug_type_path_map, true );
606 }
607
608 if ( isset( $this->_storage->plugin_main_file ) ) {
609 $plugin_main_file = $this->_storage->plugin_main_file;
610
611 if ( isset( $plugin_main_file->path ) ) {
612 $this->_storage->plugin_main_file->path = $this->get_relative_path( $this->_storage->plugin_main_file->path );
613 } else if ( isset( $plugin_main_file->prev_path ) ) {
614 $this->_storage->plugin_main_file->prev_path = $this->get_relative_path( $this->_storage->plugin_main_file->prev_path );
615 }
616 }
617
618 // Remove invalid path that is still associated with the current slug if there's any.
619 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
620 foreach ( $file_slug_map as $plugin_basename => $slug ) {
621 if ( $slug === $this->_slug &&
622 $plugin_basename !== $this->_plugin_basename &&
623 ! file_exists( $this->get_absolute_path( $plugin_basename ) )
624 ) {
625 unset( $file_slug_map[ $plugin_basename ] );
626 self::$_accounts->set_option( 'file_slug_map', $file_slug_map, true );
627
628 break;
629 }
630 }
631 }
632
633 /**
634 * @author Vova Feldman (@svovaf)
635 * @since 1.2.2.7
636 *
637 * @param string $plugin_prev_version
638 * @param string $plugin_version
639 */
640 function _after_version_update( $plugin_prev_version, $plugin_version ) {
641 if ( $this->is_theme() ) {
642 // Expire the cache of the previous tabs since the theme may
643 // have setting updates.
644 $this->_cache->expire( 'tabs' );
645 $this->_cache->expire( 'tabs_stylesheets' );
646 }
647 }
648
649 /**
650 * This action is connected to the 'plugins_loaded' hook and helps to determine
651 * if this is a new plugin installation or a plugin update.
652 *
653 * There are 3 different use-cases:
654 * 1) New plugin installation right with Freemius:
655 * 1.1 _activate_plugin_event_hook() will be executed first
656 * 1.2 Since $this->_storage->is_plugin_new_install is not set,
657 * and $this->_storage->plugin_last_version is not set,
658 * $this->_storage->is_plugin_new_install will be set to TRUE.
659 * 1.3 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install will
660 * be already set to TRUE.
661 *
662 * 2) Plugin update, didn't have Freemius before, and now have the SDK:
663 * 2.1 _activate_plugin_event_hook() will not be executed, because
664 * the activation hook do NOT fires on updates since WP 3.1.
665 * 2.2 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install will
666 * be empty, therefore, it will be set to FALSE.
667 *
668 * 3) Plugin update, had Freemius in prev version as well:
669 * 3.1 _version_updates_handler() will be executed 1st, since FS was installed
670 * before, $this->_storage->plugin_last_version will NOT be empty,
671 * therefore, $this->_storage->is_plugin_new_install will be set to FALSE.
672 * 3.2 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install is
673 * already set, therefore, it will not be modified.
674 *
675 * Use-case #3 is backward compatible, #3.1 will be executed since 1.0.9.
676 *
677 * NOTE:
678 * The only fallback of this mechanism is if an admin updates a plugin based on use-case #2,
679 * and then, the next immediate PageView is the plugin's main settings page, it will not
680 * show the opt-in right away. The reason it will happen is because Freemius execution
681 * will be turned off till the plugin is fully loaded at least once
682 * (till $this->_storage->was_plugin_loaded is TRUE).
683 *
684 * @author Vova Feldman (@svovaf)
685 * @since 1.1.9
686 *
687 */
688 function _plugins_loaded() {
689 // Update flag that plugin was loaded with Freemius at least once.
690 $this->_storage->was_plugin_loaded = true;
691
692 /**
693 * Bug fix - only set to false when it's a plugin, due to the
694 * execution sequence of the theme hooks and our methods, if
695 * this will be set for themes, Freemius will always assume
696 * it's a theme update.
697 *
698 * @author Vova Feldman (@svovaf)
699 * @since 1.2.2.2
700 */
701 if ( $this->is_plugin() &&
702 ! isset( $this->_storage->is_plugin_new_install )
703 ) {
704 $this->_storage->is_plugin_new_install = false;
705 }
706 }
707
708 /**
709 * @author Vova Feldman (@svovaf)
710 * @since 1.0.9
711 */
712 private function _register_hooks() {
713 $this->_logger->entrance();
714
715 if ( is_admin() ) {
716 if ( $this->is_plugin() ) {
717 $plugin_dir = dirname( $this->_plugin_dir_path ) . '/';
718
719 /**
720 * @since 1.2.2
721 *
722 * Hook to both free and premium version activations to support
723 * auto deactivation on the other version activation.
724 */
725 register_activation_hook(
726 $plugin_dir . $this->_free_plugin_basename,
727 array( &$this, '_activate_plugin_event_hook' )
728 );
729
730 register_activation_hook(
731 $plugin_dir . $this->premium_plugin_basename(),
732 array( &$this, '_activate_plugin_event_hook' )
733 );
734 } else {
735 add_action( 'after_switch_theme', array( &$this, '_activate_theme_event_hook' ), 10, 2 );
736
737 /**
738 * Include the required hooks to capture the theme settings' page tabs
739 * and cache them.
740 *
741 * @author Vova Feldman (@svovaf)
742 * @since 1.2.2.7
743 */
744 if ( ! $this->_cache->has_valid( 'tabs' ) ) {
745 add_action( 'admin_footer', array( &$this, '_tabs_capture' ) );
746 // Add license activation AJAX callback.
747 $this->add_ajax_action( 'store_tabs', array( &$this, '_store_tabs_ajax_action' ) );
748
749 add_action( 'admin_enqueue_scripts', array( &$this, '_store_tabs_styles' ), 9999999 );
750 }
751
752 add_action(
753 'admin_footer',
754 array( &$this, '_add_freemius_tabs' ),
755 /**
756 * The tabs JS code must be executed after the tabs capture logic (_tabs_capture()).
757 * That's why the priority is 11 while the tabs capture logic is added
758 * with priority 10.
759 *
760 * @author Vova Feldman (@svovaf)
761 */
762 11
763 );
764
765 add_action( 'admin_footer', array( &$this, '_style_premium_theme' ) );
766 }
767
768 /**
769 * Part of the mechanism to identify new plugin install vs. plugin update.
770 *
771 * @author Vova Feldman (@svovaf)
772 * @since 1.1.9
773 */
774 if ( empty( $this->_storage->was_plugin_loaded ) ) {
775 if ( $this->is_plugin() &&
776 $this->is_activation_mode( false ) &&
777 0 == did_action( 'plugins_loaded' )
778 ) {
779 add_action( 'plugins_loaded', array( &$this, '_plugins_loaded' ) );
780 } else {
781 // If was activated before, then it was already loaded before.
782 $this->_plugins_loaded();
783 }
784 }
785
786 if ( ! self::is_ajax() ) {
787 if ( ! $this->is_addon() ) {
788 add_action( 'init', array( &$this, '_add_default_submenu_items' ), WP_FS__LOWEST_PRIORITY );
789 }
790 }
791 }
792
793 if ( $this->is_plugin() ) {
794 register_deactivation_hook( $this->_plugin_main_file_path, array( &$this, '_deactivate_plugin_hook' ) );
795 }
796
797 if ( $this->is_theme() && self::is_customizer() ) {
798 // Register customizer upsell.
799 add_action( 'customize_register', array( &$this, '_customizer_register' ) );
800 }
801
802 add_action( 'init', array( &$this, '_redirect_on_clicked_menu_link' ), WP_FS__LOWEST_PRIORITY );
803
804 add_action( 'admin_init', array( &$this, '_add_tracking_links' ) );
805 add_action( 'admin_init', array( &$this, '_add_license_activation' ) );
806 $this->add_ajax_action( 'update_billing', array( &$this, '_update_billing_ajax_action' ) );
807 $this->add_ajax_action( 'start_trial', array( &$this, '_start_trial_ajax_action' ) );
808
809 $this->add_ajax_action( 'install_premium_version', array(
810 &$this,
811 '_install_premium_version_ajax_action'
812 ) );
813
814 $this->add_ajax_action( 'submit_affiliate_application', array( &$this, '_submit_affiliate_application' ) );
815
816 $this->add_action( 'after_plans_sync', array( &$this, '_check_for_trial_plans' ) );
817
818 $this->add_action( 'sdk_version_update', array( &$this, '_data_migration' ), WP_FS__DEFAULT_PRIORITY, 2 );
819 $this->add_action( 'plugin_version_update', array( &$this, '_after_version_update' ), WP_FS__DEFAULT_PRIORITY, 2 );
820 $this->add_filter( 'after_code_type_change', array( &$this, '_after_code_type_change' ) );
821
822 add_action( 'admin_init', array( &$this, '_add_trial_notice' ) );
823 add_action( 'admin_init', array( &$this, '_add_affiliate_program_notice' ) );
824 add_action( 'admin_init', array( &$this, '_enqueue_common_css' ) );
825
826 /**
827 * Handle request to reset anonymous mode for `get_reconnect_url()`.
828 *
829 * @author Vova Feldman (@svovaf)
830 * @since 1.2.1.5
831 */
832 if ( fs_request_is_action( 'reset_anonymous_mode' ) &&
833 $this->get_unique_affix() === fs_request_get( 'fs_unique_affix' )
834 ) {
835 add_action( 'admin_init', array( &$this, 'connect_again' ) );
836 }
837 }
838
839 /**
840 * Keeping the uninstall hook registered for free or premium plugin version may result to a fatal error that
841 * could happen when a user tries to uninstall either version while one of them is still active. Uninstalling a
842 * plugin will trigger inclusion of the free or premium version and if one of them is active during the
843 * uninstallation, a fatal error may occur in case the plugin's class or functions are already defined.
844 *
845 * @author Leo Fajardo (leorw)
846 *
847 * @since 1.2.0
848 */
849 private function unregister_uninstall_hook() {
850 $uninstallable_plugins = (array) get_option( 'uninstall_plugins' );
851 unset( $uninstallable_plugins[ $this->_free_plugin_basename ] );
852 unset( $uninstallable_plugins[ $this->premium_plugin_basename() ] );
853
854 update_option( 'uninstall_plugins', $uninstallable_plugins );
855 }
856
857 /**
858 * @since 1.2.0 Invalidate module's main file cache, otherwise, FS_Plugin_Updater will not fetch updates.
859 */
860 private function clear_module_main_file_cache() {
861 if ( ! isset( $this->_storage->plugin_main_file ) ||
862 empty( $this->_storage->plugin_main_file->path )
863 ) {
864 return;
865 }
866
867 $plugin_main_file = clone $this->_storage->plugin_main_file;
868
869 // Store cached path (2nd layer cache).
870 $plugin_main_file->prev_path = $plugin_main_file->path;
871
872 // Clear cached path.
873 unset( $plugin_main_file->path );
874
875 $this->_storage->plugin_main_file = $plugin_main_file;
876
877 /**
878 * Clear global cached path.
879 *
880 * @author Leo Fajardo (@leorw)
881 * @since 1.2.2
882 */
883 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map' );
884 unset( $id_slug_type_path_map[ $this->_module_id ]['path'] );
885 self::$_accounts->set_option( 'id_slug_type_path_map', $id_slug_type_path_map, true );
886 }
887
888 /**
889 * @author Vova Feldman (@svovaf)
890 * @since 1.0.9
891 */
892 private function _register_account_hooks() {
893 if ( ! is_admin() ) {
894 return;
895 }
896
897 /**
898 * Always show the deactivation feedback form since we added
899 * automatic free version deactivation upon premium code activation.
900 *
901 * @since 1.2.1.6
902 */
903 $this->add_ajax_action(
904 'submit_uninstall_reason',
905 array( &$this, '_submit_uninstall_reason_action' )
906 );
907
908 if ( ( $this->is_plugin() && self::is_plugins_page() ) ||
909 ( $this->is_theme() && self::is_themes_page() )
910 ) {
911 add_action( 'admin_footer', array( &$this, '_add_deactivation_feedback_dialog_box' ) );
912 }
913 }
914
915 /**
916 * Leverage backtrace to find caller plugin file path.
917 *
918 * @author Vova Feldman (@svovaf)
919 * @since 1.0.6
920 *
921 * @param bool $is_init Is initiation sequence.
922 *
923 * @return string
924 */
925 private function _find_caller_plugin_file( $is_init = false ) {
926 // Try to load the cached value of the file path.
927 if ( isset( $this->_storage->plugin_main_file ) ) {
928 $plugin_main_file = $this->_storage->plugin_main_file;
929 if ( isset( $plugin_main_file->path ) ) {
930 $absolute_path = $this->get_absolute_path( $plugin_main_file->path );
931 if ( file_exists( $absolute_path ) ) {
932 return $absolute_path;
933 }
934 }
935 }
936
937 /**
938 * @since 1.2.1
939 *
940 * `clear_module_main_file_cache()` is clearing the plugin's cached path on
941 * deactivation. Therefore, if any plugin/theme was initiating `Freemius`
942 * with that plugin's slug, it was overriding the empty plugin path with a wrong path.
943 *
944 * So, we've added a special mechanism with a 2nd layer of cache that uses `prev_path`
945 * when the class instantiator isn't the module.
946 */
947 if ( ! $is_init ) {
948 // Fetch prev path cache.
949 if ( isset( $this->_storage->plugin_main_file ) &&
950 isset( $this->_storage->plugin_main_file->prev_path )
951 ) {
952 $absolute_path = $this->get_absolute_path( $this->_storage->plugin_main_file->prev_path );
953 if ( file_exists( $absolute_path ) ) {
954 return $absolute_path;
955 }
956 }
957
958 wp_die(
959 $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' ) .
960 " Module: {$this->_slug}; SDK: " . WP_FS__SDK_VERSION . ";",
961 $this->get_text_inline( 'Error', 'error' ),
962 array( 'back_link' => true )
963 );
964 }
965
966 /**
967 * @since 1.2.1
968 *
969 * Only the original instantiator that calls dynamic_init can modify the module's path.
970 */
971 // Find caller module.
972 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
973 $this->_storage->plugin_main_file = (object) array(
974 'path' => $id_slug_type_path_map[ $this->_module_id ]['path'],
975 );
976
977 return $this->get_absolute_path( $id_slug_type_path_map[ $this->_module_id ]['path'] );
978 }
979
980 /**
981 * @author Leo Fajardo (@leorw)
982 * @since 1.2.3
983 *
984 * @param string $path
985 *
986 * @return string
987 */
988 private function get_relative_path( $path ) {
989 $module_root_dir = $this->get_module_root_dir_path();
990 if ( 0 === strpos( $path, $module_root_dir ) ) {
991 $path = substr( $path, strlen( $module_root_dir ) );
992 }
993
994 return $path;
995 }
996
997 /**
998 * @author Leo Fajardo (@leorw)
999 * @since 1.2.3
1000 *
1001 * @param string $path
1002 * @param string|bool $module_type
1003 *
1004 * @return string
1005 */
1006 private function get_absolute_path( $path, $module_type = false ) {
1007 $module_root_dir = $this->get_module_root_dir_path( $module_type );
1008 if ( 0 !== strpos( $path, $module_root_dir ) ) {
1009 $path = fs_normalize_path( $module_root_dir . $path );
1010 }
1011
1012 return $path;
1013 }
1014
1015 /**
1016 * @author Leo Fajardo (@leorw)
1017 * @since 1.2.3
1018 *
1019 * @param string|bool $module_type
1020 *
1021 * @return string
1022 */
1023 private function get_module_root_dir_path( $module_type = false ) {
1024 $is_plugin = empty( $module_type ) ?
1025 $this->is_plugin() :
1026 ( WP_FS__MODULE_TYPE_PLUGIN === $module_type );
1027
1028 return fs_normalize_path( trailingslashit( $is_plugin ?
1029 WP_PLUGIN_DIR :
1030 get_theme_root() ) );
1031 }
1032
1033 /**
1034 * @author Leo Fajardo (@leorw)
1035 *
1036 * @param number $module_id
1037 * @param string $slug
1038 *
1039 * @since 1.2.2
1040 */
1041 private function store_id_slug_type_path_map( $module_id, $slug ) {
1042 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
1043
1044 $store_option = false;
1045
1046 if ( ! isset( $id_slug_type_path_map[ $module_id ] ) ) {
1047 $id_slug_type_path_map[ $module_id ] = array(
1048 'slug' => $slug
1049 );
1050
1051 $store_option = true;
1052 }
1053
1054 if ( ! isset( $id_slug_type_path_map[ $module_id ]['path'] ) ||
1055 /**
1056 * This verification is for cases when suddenly the same module
1057 * is installed but with a different folder name.
1058 *
1059 * @author Vova Feldman (@svovaf)
1060 * @since 1.2.3
1061 */
1062 ! file_exists( $this->get_absolute_path(
1063 $id_slug_type_path_map[ $module_id ]['path'],
1064 $id_slug_type_path_map[ $module_id ]['type']
1065 ) )
1066 ) {
1067 $caller_main_file_and_type = $this->get_caller_main_file_and_type();
1068
1069 $id_slug_type_path_map[ $module_id ]['type'] = $caller_main_file_and_type->module_type;
1070 $id_slug_type_path_map[ $module_id ]['path'] = $caller_main_file_and_type->path;
1071
1072 $store_option = true;
1073 }
1074
1075 if ( $store_option ) {
1076 self::$_accounts->set_option( 'id_slug_type_path_map', $id_slug_type_path_map, true );
1077 }
1078 }
1079
1080 /**
1081 * Identifies the caller type: plugin or theme.
1082 *
1083 * @author Leo Fajardo (@leorw)
1084 * @since 1.2.2
1085 *
1086 * @author Vova Feldman (@svovaf)
1087 * @since 1.2.2.3 Find the earliest module in the call stack that calls to the SDK. This fix is for cases when
1088 * add-ons are relying on loading the SDK from the parent module, and also allows themes including the
1089 * SDK an internal file instead of directly from functions.php.
1090 * @since 1.2.1.7 Knows how to handle cases when an add-on includes the parent module logic.
1091 */
1092 private function get_caller_main_file_and_type() {
1093 self::require_plugin_essentials();
1094
1095 $all_plugins = get_plugins();
1096 $all_plugins_paths = array();
1097
1098 // Get active plugin's main files real full names (might be symlinks).
1099 foreach ( $all_plugins as $relative_path => &$data ) {
1100 if ( false === strpos( fs_normalize_path( $relative_path ), '/' ) ) {
1101 /**
1102 * Ignore plugins that don't have a folder (e.g. Hello Dolly) since they
1103 * can't really include the SDK.
1104 *
1105 * @author Vova Feldman
1106 * @since 1.2.1.7
1107 */
1108 continue;
1109 }
1110
1111 $all_plugins_paths[] = fs_normalize_path( realpath( WP_PLUGIN_DIR . '/' . $relative_path ) );
1112 }
1113
1114 $caller_file_candidate = false;
1115 $caller_map = array();
1116 $module_type = WP_FS__MODULE_TYPE_PLUGIN;
1117 $themes_dir = fs_normalize_path( get_theme_root() );
1118
1119 for ( $i = 1, $bt = debug_backtrace(), $len = count( $bt ); $i < $len; $i ++ ) {
1120 if ( empty( $bt[ $i ]['file'] ) ) {
1121 continue;
1122 }
1123
1124 if ( $i > 1 && ! empty( $bt[ $i - 1 ]['file'] ) && $bt[ $i ]['file'] === $bt[ $i - 1 ]['file'] ) {
1125 // If file same as the prev file in the stack, skip it.
1126 continue;
1127 }
1128
1129 if ( ! empty( $bt[ $i ]['function'] ) && in_array( $bt[ $i ]['function'], array(
1130 'do_action',
1131 'apply_filter',
1132 // The string split is stupid, but otherwise, theme check
1133 // throws info notices.
1134 'requir' . 'e_once',
1135 'requir' . 'e',
1136 'includ' . 'e_once',
1137 'includ' . 'e'
1138 ) )
1139 ) {
1140 // Ignore call stack hooks and files inclusion.
1141 continue;
1142 }
1143
1144 $caller_file_path = fs_normalize_path( $bt[ $i ]['file'] );
1145
1146 if ( 'functions.php' === basename( $caller_file_path ) ) {
1147 /**
1148 * 1. Assumes that theme's starting execution file is functions.php.
1149 * 2. This complex logic fixes symlink issues (e.g. with Vargant).
1150 *
1151 * @author Vova Feldman (@svovaf)
1152 * @since 1.2.2.5
1153 */
1154
1155 if ( $caller_file_path == fs_normalize_path( realpath( trailingslashit( $themes_dir ) . basename( dirname( $caller_file_path ) ) . '/' . basename( $caller_file_path ) ) ) ) {
1156 $module_type = WP_FS__MODULE_TYPE_THEME;
1157
1158 /**
1159 * Relative path of the theme, e.g.:
1160 * `my-theme/functions.php`
1161 *
1162 * @author Leo Fajardo (@leorw)
1163 */
1164 $caller_file_candidate = basename( dirname( $caller_file_path ) ) .
1165 '/' .
1166 basename( $caller_file_path );
1167
1168 continue;
1169 }
1170 }
1171
1172 $caller_file_hash = md5( $caller_file_path );
1173
1174 if ( ! isset( $caller_map[ $caller_file_hash ] ) ) {
1175 foreach ( $all_plugins_paths as $plugin_path ) {
1176 if ( false !== strpos( $caller_file_path, fs_normalize_path( dirname( $plugin_path ) . '/' ) ) ) {
1177 $caller_map[ $caller_file_hash ] = fs_normalize_path( $plugin_path );
1178 break;
1179 }
1180 }
1181 }
1182
1183 if ( isset( $caller_map[ $caller_file_hash ] ) ) {
1184 $module_type = WP_FS__MODULE_TYPE_PLUGIN;
1185 $caller_file_candidate = plugin_basename( $caller_map[ $caller_file_hash ] );
1186 }
1187 }
1188
1189 return (object) array(
1190 'module_type' => $module_type,
1191 'path' => $caller_file_candidate
1192 );
1193 }
1194
1195 #----------------------------------------------------------------------------------
1196 #region Deactivation Feedback Form
1197 #----------------------------------------------------------------------------------
1198
1199 /**
1200 * Displays a confirmation and feedback dialog box when the user clicks on the "Deactivate" link on the plugins
1201 * page.
1202 *
1203 * @author Vova Feldman (@svovaf)
1204 * @author Leo Fajardo (@leorw)
1205 * @since 1.1.2
1206 */
1207 function _add_deactivation_feedback_dialog_box() {
1208 /* Check the type of user:
1209 * 1. Long-term (long-term)
1210 * 2. Non-registered and non-anonymous short-term (non-registered-and-non-anonymous-short-term).
1211 * 3. Short-term (short-term)
1212 */
1213 $is_long_term_user = true;
1214
1215 // Check if the site is at least 2 days old.
1216 $time_installed = $this->_storage->install_timestamp;
1217
1218 // Difference in seconds.
1219 $date_diff = time() - $time_installed;
1220
1221 // Convert seconds to days.
1222 $date_diff_days = floor( $date_diff / ( 60 * 60 * 24 ) );
1223
1224 if ( $date_diff_days < 2 ) {
1225 $is_long_term_user = false;
1226 }
1227
1228 $is_long_term_user = $this->apply_filters( 'is_long_term_user', $is_long_term_user );
1229
1230 if ( $is_long_term_user ) {
1231 $user_type = 'long-term';
1232 } else {
1233 if ( ! $this->is_registered() && ! $this->is_anonymous() ) {
1234 $user_type = 'non-registered-and-non-anonymous-short-term';
1235 } else {
1236 $user_type = 'short-term';
1237 }
1238 }
1239
1240 $uninstall_reasons = $this->_get_uninstall_reasons( $user_type );
1241
1242 // Load the HTML template for the deactivation feedback dialog box.
1243 $vars = array(
1244 'reasons' => $uninstall_reasons,
1245 'id' => $this->_module_id
1246 );
1247
1248 /**
1249 * @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.
1250 */
1251 fs_require_template( 'forms/deactivation/form.php', $vars );
1252 }
1253
1254 /**
1255 * @author Leo Fajardo (leorw)
1256 * @since 1.1.2
1257 *
1258 * @param string $user_type
1259 *
1260 * @return array The uninstall reasons for the specified user type.
1261 */
1262 function _get_uninstall_reasons( $user_type = 'long-term' ) {
1263 $module_type = $this->_module_type;
1264
1265 $internal_message_template_var = array(
1266 'id' => $this->_module_id
1267 );
1268
1269 if ( $this->is_registered() && false !== $this->get_plan() && $this->get_plan()->has_technical_support() ) {
1270 $contact_support_template = fs_get_template( 'forms/deactivation/contact.php', $internal_message_template_var );
1271 } else {
1272 $contact_support_template = '';
1273 }
1274
1275 $reason_found_better_plugin = array(
1276 'id' => self::REASON_FOUND_A_BETTER_PLUGIN,
1277 'text' => sprintf( $this->get_text_inline( 'I found a better %s', 'reason-found-a-better-plugin' ), $module_type ),
1278 'input_type' => 'textfield',
1279 'input_placeholder' => sprintf( $this->get_text_inline( "What's the %s's name?", 'placeholder-plugin-name' ), $module_type ),
1280 );
1281
1282 $reason_temporary_deactivation = array(
1283 'id' => self::REASON_TEMPORARY_DEACTIVATION,
1284 'text' => sprintf(
1285 $this->get_text_inline( "It's a temporary %s. I'm just debugging an issue.", 'reason-temporary-x' ),
1286 strtolower( $this->is_plugin() ?
1287 $this->get_text_inline( 'Deactivation', 'deactivation' ) :
1288 $this->get_text_inline( 'Theme Switch', 'theme-switch' )
1289 )
1290 ),
1291 'input_type' => '',
1292 'input_placeholder' => ''
1293 );
1294
1295 $reason_other = array(
1296 'id' => self::REASON_OTHER,
1297 'text' => $this->get_text_inline( 'Other', 'reason-other' ),
1298 'input_type' => 'textfield',
1299 'input_placeholder' => ''
1300 );
1301
1302 $long_term_user_reasons = array(
1303 array(
1304 'id' => self::REASON_NO_LONGER_NEEDED,
1305 'text' => sprintf( $this->get_text_inline( 'I no longer need the %s', 'reason-no-longer-needed' ), $module_type ),
1306 'input_type' => '',
1307 'input_placeholder' => ''
1308 ),
1309 $reason_found_better_plugin,
1310 array(
1311 'id' => self::REASON_NEEDED_FOR_A_SHORT_PERIOD,
1312 'text' => sprintf( $this->get_text_inline( 'I only needed the %s for a short period', 'reason-needed-for-a-short-period' ), $module_type ),
1313 'input_type' => '',
1314 'input_placeholder' => ''
1315 ),
1316 array(
1317 'id' => self::REASON_BROKE_MY_SITE,
1318 'text' => sprintf( $this->get_text_inline( 'The %s broke my site', 'reason-broke-my-site' ), $module_type ),
1319 'input_type' => '',
1320 'input_placeholder' => '',
1321 'internal_message' => $contact_support_template
1322 ),
1323 array(
1324 'id' => self::REASON_SUDDENLY_STOPPED_WORKING,
1325 'text' => sprintf( $this->get_text_inline( 'The %s suddenly stopped working', 'reason-suddenly-stopped-working' ), $module_type ),
1326 'input_type' => '',
1327 'input_placeholder' => '',
1328 'internal_message' => $contact_support_template
1329 )
1330 );
1331
1332 if ( $this->is_paying() ) {
1333 $long_term_user_reasons[] = array(
1334 'id' => self::REASON_CANT_PAY_ANYMORE,
1335 'text' => $this->get_text_inline( "I can't pay for it anymore", 'reason-cant-pay-anymore' ),
1336 'input_type' => 'textfield',
1337 'input_placeholder' => $this->get_text_inline( 'What price would you feel comfortable paying?', 'placeholder-comfortable-price' )
1338 );
1339 }
1340
1341 $reason_dont_share_info = array(
1342 'id' => self::REASON_DONT_LIKE_TO_SHARE_MY_INFORMATION,
1343 'text' => $this->get_text_inline( "I don't like to share my information with you", 'reason-dont-like-to-share-my-information' ),
1344 'input_type' => '',
1345 'input_placeholder' => ''
1346 );
1347
1348 /**
1349 * If the current user has selected the "don't share data" reason in the deactivation feedback modal, inform the
1350 * user by showing additional message that he doesn't have to share data and can just choose to skip the opt-in
1351 * (the Skip button is included in the message to show). This message will only be shown if anonymous mode is
1352 * enabled and the user's account is currently not in pending activation state (similar to the way the Skip
1353 * button in the opt-in form is shown/hidden).
1354 */
1355 if ( $this->is_enable_anonymous() && ! $this->is_pending_activation() ) {
1356 $reason_dont_share_info['internal_message'] = fs_get_template( 'forms/deactivation/retry-skip.php', $internal_message_template_var );
1357 }
1358
1359 $uninstall_reasons = array(
1360 'long-term' => $long_term_user_reasons,
1361 'non-registered-and-non-anonymous-short-term' => array(
1362 array(
1363 'id' => self::REASON_DIDNT_WORK,
1364 'text' => sprintf( $this->get_text_inline( "The %s didn't work", 'reason-didnt-work' ), $module_type ),
1365 'input_type' => '',
1366 'input_placeholder' => ''
1367 ),
1368 $reason_dont_share_info,
1369 $reason_found_better_plugin
1370 ),
1371 'short-term' => array(
1372 array(
1373 'id' => self::REASON_COULDNT_MAKE_IT_WORK,
1374 'text' => $this->get_text_inline( "I couldn't understand how to make it work", 'reason-couldnt-make-it-work' ),
1375 'input_type' => '',
1376 'input_placeholder' => '',
1377 'internal_message' => $contact_support_template
1378 ),
1379 $reason_found_better_plugin,
1380 array(
1381 'id' => self::REASON_GREAT_BUT_NEED_SPECIFIC_FEATURE,
1382 '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 ),
1383 'input_type' => 'textarea',
1384 'input_placeholder' => $this->get_text_inline( 'What feature?', 'placeholder-feature' )
1385 ),
1386 array(
1387 'id' => self::REASON_NOT_WORKING,
1388 'text' => sprintf( $this->get_text_inline( 'The %s is not working', 'reason-not-working' ), $module_type ),
1389 'input_type' => 'textarea',
1390 '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' )
1391 ),
1392 array(
1393 'id' => self::REASON_NOT_WHAT_I_WAS_LOOKING_FOR,
1394 'text' => $this->get_text_inline( "It's not what I was looking for", 'reason-not-what-i-was-looking-for' ),
1395 'input_type' => 'textarea',
1396 'input_placeholder' => $this->get_text_inline( "What you've been looking for?", 'placeholder-what-youve-been-looking-for' )
1397 ),
1398 array(
1399 'id' => self::REASON_DIDNT_WORK_AS_EXPECTED,
1400 'text' => sprintf( $this->get_text_inline( "The %s didn't work as expected", 'reason-didnt-work-as-expected' ), $module_type ),
1401 'input_type' => 'textarea',
1402 'input_placeholder' => $this->get_text_inline( 'What did you expect?', 'placeholder-what-did-you-expect' )
1403 )
1404 )
1405 );
1406
1407 // Randomize the reasons for the current user type.
1408 shuffle( $uninstall_reasons[ $user_type ] );
1409
1410 // Keep the following reasons as the last items in the list.
1411 $uninstall_reasons[ $user_type ][] = $reason_temporary_deactivation;
1412 $uninstall_reasons[ $user_type ][] = $reason_other;
1413
1414 $uninstall_reasons = $this->apply_filters( 'uninstall_reasons', $uninstall_reasons );
1415
1416 return $uninstall_reasons[ $user_type ];
1417 }
1418
1419 /**
1420 * Called after the user has submitted his reason for deactivating the plugin.
1421 *
1422 * @author Leo Fajardo (@leorw)
1423 * @since 1.1.2
1424 */
1425 function _submit_uninstall_reason_action() {
1426 $this->_logger->entrance();
1427
1428 $this->check_ajax_referer( 'submit_uninstall_reason' );
1429
1430 $reason_id = fs_request_get( 'reason_id' );
1431
1432 // Check if the given reason ID is an unsigned integer.
1433 if ( ! ctype_digit( $reason_id ) ) {
1434 exit;
1435 }
1436
1437 $reason_info = trim( fs_request_get( 'reason_info', '' ) );
1438 if ( ! empty( $reason_info ) ) {
1439 $reason_info = substr( $reason_info, 0, 128 );
1440 }
1441
1442 $reason = (object) array(
1443 'id' => $reason_id,
1444 'info' => $reason_info,
1445 'is_anonymous' => fs_request_get_bool( 'is_anonymous' )
1446 );
1447
1448 $this->_storage->store( 'uninstall_reason', $reason );
1449
1450 /**
1451 * If the module type is "theme", trigger the uninstall event here (on theme deactivation) since themes do
1452 * not support uninstall hook.
1453 *
1454 * @author Leo Fajardo (@leorw)
1455 * @since 1.2.2
1456 */
1457 if ( $this->is_theme() ) {
1458 $this->_uninstall_plugin_event( false );
1459 $this->remove_sdk_reference();
1460 }
1461
1462 // Print '1' for successful operation.
1463 echo 1;
1464 exit;
1465 }
1466
1467 #endregion
1468
1469 #----------------------------------------------------------------------------------
1470 #region Instance
1471 #----------------------------------------------------------------------------------
1472
1473 /**
1474 * Main singleton instance.
1475 *
1476 * @author Vova Feldman (@svovaf)
1477 * @since 1.0.0
1478 *
1479 * @param number $module_id
1480 * @param string|bool $slug
1481 * @param bool $is_init Is initiation sequence.
1482 *
1483 * @return Freemius|false
1484 */
1485 static function instance( $module_id, $slug = false, $is_init = false ) {
1486 if ( empty( $module_id ) ) {
1487 return false;
1488 }
1489
1490 if ( ! is_numeric( $module_id ) ) {
1491 if ( ! $is_init && true === $slug ) {
1492 $is_init = true;
1493 }
1494
1495 $slug = $module_id;
1496
1497 $module = FS_Plugin_Manager::instance( $slug )->get();
1498
1499 if ( is_object( $module ) ) {
1500 $module_id = $module->id;
1501 }
1502 }
1503
1504 $key = 'm_' . $module_id;
1505
1506 if ( ! isset( self::$_instances[ $key ] ) ) {
1507 if ( 0 === count( self::$_instances ) ) {
1508 self::_load_required_static();
1509 }
1510
1511 self::$_instances[ $key ] = new Freemius( $module_id, $slug, $is_init );
1512 }
1513
1514 return self::$_instances[ $key ];
1515 }
1516
1517 /**
1518 * @author Vova Feldman (@svovaf)
1519 * @since 1.0.6
1520 *
1521 * @param number $addon_id
1522 *
1523 * @return bool
1524 */
1525 private static function has_instance( $addon_id ) {
1526 return isset( self::$_instances[ 'm_' . $addon_id ] );
1527 }
1528
1529 /**
1530 * @author Leo Fajardo (@leorw)
1531 * @since 1.2.2
1532 *
1533 * @param string|number $id_or_slug
1534 *
1535 * @return number|false
1536 */
1537 private static function get_module_id( $id_or_slug ) {
1538 if ( is_numeric( $id_or_slug ) ) {
1539 return $id_or_slug;
1540 }
1541
1542 foreach ( self::$_instances as $instance ) {
1543 if ( $instance->is_plugin() && ( $id_or_slug === $instance->get_slug() ) ) {
1544 return $instance->get_id();
1545 }
1546 }
1547
1548 return false;
1549 }
1550
1551 /**
1552 * @author Vova Feldman (@svovaf)
1553 * @since 1.0.6
1554 *
1555 * @param number $id
1556 *
1557 * @return false|Freemius
1558 */
1559 static function get_instance_by_id( $id ) {
1560 return isset ( self::$_instances[ 'm_' . $id ] ) ?
1561 self::$_instances[ 'm_' . $id ] :
1562 false;
1563 }
1564
1565 /**
1566 *
1567 * @author Vova Feldman (@svovaf)
1568 * @since 1.0.1
1569 *
1570 * @param $plugin_file
1571 *
1572 * @return false|Freemius
1573 */
1574 static function get_instance_by_file( $plugin_file ) {
1575 $slug = self::find_slug_by_basename( $plugin_file );
1576
1577 return ( false !== $slug ) ?
1578 self::instance( self::get_module_id( $slug ) ) :
1579 false;
1580 }
1581
1582 /**
1583 * @author Vova Feldman (@svovaf)
1584 * @since 1.0.6
1585 *
1586 * @return false|Freemius
1587 */
1588 function get_parent_instance() {
1589 return self::get_instance_by_id( $this->_plugin->parent_plugin_id );
1590 }
1591
1592 /**
1593 * @author Vova Feldman (@svovaf)
1594 * @since 1.0.6
1595 *
1596 * @param string|number $id_or_slug
1597 *
1598 * @return false|Freemius
1599 */
1600 function get_addon_instance( $id_or_slug ) {
1601 $addon_id = self::get_module_id( $id_or_slug );
1602
1603 return self::instance( $addon_id );
1604 }
1605
1606 #endregion ------------------------------------------------------------------
1607
1608 /**
1609 * @author Vova Feldman (@svovaf)
1610 * @since 1.0.6
1611 *
1612 * @return bool
1613 */
1614 function is_parent_plugin_installed() {
1615 $is_active = self::has_instance( $this->_plugin->parent_plugin_id );
1616
1617 if ( $is_active ) {
1618 return true;
1619 }
1620
1621 /**
1622 * Parent module might be a theme. If that's the case, the add-on's FS
1623 * instance will be loaded prior to the theme's FS instance, therefore,
1624 * we need to check if it's active with a "look ahead".
1625 *
1626 * @author Vova Feldman
1627 * @since 1.2.2.3
1628 */
1629 global $fs_active_plugins;
1630 if ( is_object( $fs_active_plugins ) && is_array( $fs_active_plugins->plugins ) ) {
1631 $active_theme = wp_get_theme();
1632
1633 foreach ( $fs_active_plugins->plugins as $sdk => $module ) {
1634 if ( WP_FS__MODULE_TYPE_THEME === $module->type ) {
1635 if ( $module->plugin_path == $active_theme->get_stylesheet() ) {
1636 // Parent module is a theme and it's currently active.
1637 return true;
1638 }
1639 }
1640 }
1641 }
1642
1643 return false;
1644 }
1645
1646 /**
1647 * Check if add-on parent plugin in activation mode.
1648 *
1649 * @author Vova Feldman (@svovaf)
1650 * @since 1.0.7
1651 *
1652 * @return bool
1653 */
1654 function is_parent_in_activation() {
1655 $parent_fs = $this->get_parent_instance();
1656 if ( ! is_object( $parent_fs ) ) {
1657 return false;
1658 }
1659
1660 return ( $parent_fs->is_activation_mode() );
1661 }
1662
1663 /**
1664 * Is plugin in activation mode.
1665 *
1666 * @author Vova Feldman (@svovaf)
1667 * @since 1.0.7
1668 *
1669 * @param bool $and_on
1670 *
1671 * @return bool
1672 */
1673 function is_activation_mode( $and_on = true ) {
1674 return (
1675 ( $this->is_on() || ! $and_on ) &&
1676 ( ! $this->is_registered() || ( $this->is_only_premium() && ! $this->has_features_enabled_license() ) ) &&
1677 ( ! $this->is_enable_anonymous() ||
1678 ( ! $this->is_anonymous() && ! $this->is_pending_activation() ) )
1679 );
1680 }
1681
1682 /**
1683 * Check if current page is the opt-in/pending-activation page.
1684 *
1685 * @author Vova Feldman (@svovaf)
1686 * @since 1.2.1.7
1687 *
1688 * @return bool
1689 */
1690 function is_activation_page() {
1691 if ( $this->_menu->is_main_settings_page() ) {
1692 return true;
1693 }
1694
1695 if ( ! $this->is_activation_mode() ) {
1696 return false;
1697 }
1698
1699 // Check if current page is matching the activation page.
1700 return $this->is_matching_url( $this->get_activation_url() );
1701 }
1702
1703 /**
1704 * Check if URL path's are matching and that all querystring
1705 * arguments of the $sub_url exist in the $url with the same values.
1706 *
1707 * WARNING:
1708 * 1. This method doesn't check if the sub/domain are matching.
1709 * 2. Ignore case sensitivity.
1710 *
1711 * @author Vova Feldman (@svovaf)
1712 * @since 1.2.1.7
1713 *
1714 * @param string $sub_url
1715 * @param string $url If argument is not set, check if the sub_url matching the current's page URL.
1716 *
1717 * @return bool
1718 */
1719 private function is_matching_url( $sub_url, $url = '' ) {
1720 if ( empty( $url ) ) {
1721 $url = $_SERVER['REQUEST_URI'];
1722 }
1723
1724 $url = strtolower( $url );
1725 $sub_url = strtolower( $sub_url );
1726
1727 if ( parse_url( $sub_url, PHP_URL_PATH ) !== parse_url( $url, PHP_URL_PATH ) ) {
1728 // Different path - DO NOT OVERRIDE PAGE.
1729 return false;
1730 }
1731
1732 $url_params = array();
1733 parse_str( parse_url( $url, PHP_URL_QUERY ), $url_params );
1734
1735 $sub_url_params = array();
1736 parse_str( parse_url( $sub_url, PHP_URL_QUERY ), $sub_url_params );
1737
1738 foreach ( $sub_url_params as $key => $val ) {
1739 if ( ! isset( $url_params[ $key ] ) || $val != $url_params[ $key ] ) {
1740 // Not matching query string - DO NOT OVERRIDE PAGE.
1741 return false;
1742 }
1743 }
1744
1745 return true;
1746 }
1747
1748 /**
1749 * Get collection of all active plugins.
1750 *
1751 * @author Vova Feldman (@svovaf)
1752 * @since 1.0.9
1753 *
1754 * @return array[string]array
1755 */
1756 private static function get_active_plugins() {
1757 self::require_plugin_essentials();
1758
1759 $active_plugin = array();
1760 $all_plugins = get_plugins();
1761 $active_plugins_basenames = get_option( 'active_plugins' );
1762
1763 foreach ( $active_plugins_basenames as $plugin_basename ) {
1764 $active_plugin[ $plugin_basename ] = $all_plugins[ $plugin_basename ];
1765 }
1766
1767 return $active_plugin;
1768 }
1769
1770 /**
1771 * Get collection of all plugins.
1772 *
1773 * @author Vova Feldman (@svovaf)
1774 * @since 1.1.8
1775 *
1776 * @return array Key is the plugin file path and the value is an array of the plugin data.
1777 */
1778 private static function get_all_plugins() {
1779 self::require_plugin_essentials();
1780
1781 $all_plugins = get_plugins();
1782 $active_plugins_basenames = get_option( 'active_plugins' );
1783
1784 foreach ( $all_plugins as $basename => &$data ) {
1785 // By default set to inactive (next foreach update the active plugins).
1786 $data['is_active'] = false;
1787 // Enrich with plugin slug.
1788 $data['slug'] = self::get_plugin_slug( $basename );
1789 }
1790
1791 // Flag active plugins.
1792 foreach ( $active_plugins_basenames as $basename ) {
1793 if ( isset( $all_plugins[ $basename ] ) ) {
1794 $all_plugins[ $basename ]['is_active'] = true;
1795 }
1796 }
1797
1798 return $all_plugins;
1799 }
1800
1801
1802 /**
1803 * Cached result of get_site_transient( 'update_plugins' )
1804 *
1805 * @author Vova Feldman (@svovaf)
1806 * @since 1.1.8
1807 *
1808 * @var object
1809 */
1810 private static $_plugins_info;
1811
1812 /**
1813 * Helper function to get specified plugin's slug.
1814 *
1815 * @author Vova Feldman (@svovaf)
1816 * @since 1.1.8
1817 *
1818 * @param $basename
1819 *
1820 * @return string
1821 */
1822 private static function get_plugin_slug( $basename ) {
1823 if ( ! isset( self::$_plugins_info ) ) {
1824 self::$_plugins_info = get_site_transient( 'update_plugins' );
1825 }
1826
1827 $slug = '';
1828
1829 if ( is_object( self::$_plugins_info ) ) {
1830 if ( isset( self::$_plugins_info->no_update ) &&
1831 isset( self::$_plugins_info->no_update[ $basename ] ) &&
1832 ! empty( self::$_plugins_info->no_update[ $basename ]->slug )
1833 ) {
1834 $slug = self::$_plugins_info->no_update[ $basename ]->slug;
1835 } else if ( isset( self::$_plugins_info->response ) &&
1836 isset( self::$_plugins_info->response[ $basename ] ) &&
1837 ! empty( self::$_plugins_info->response[ $basename ]->slug )
1838 ) {
1839 $slug = self::$_plugins_info->response[ $basename ]->slug;
1840 }
1841 }
1842
1843 if ( empty( $slug ) ) {
1844 // Try to find slug from FS data.
1845 $slug = self::find_slug_by_basename( $basename );
1846 }
1847
1848 if ( empty( $slug ) ) {
1849 // Fallback to plugin's folder name.
1850 $slug = dirname( $basename );
1851 }
1852
1853 return $slug;
1854 }
1855
1856 private static $_statics_loaded = false;
1857
1858 /**
1859 * Load static resources.
1860 *
1861 * @author Vova Feldman (@svovaf)
1862 * @since 1.0.1
1863 */
1864 private static function _load_required_static() {
1865 if ( self::$_statics_loaded ) {
1866 return;
1867 }
1868
1869 self::$_static_logger = FS_Logger::get_logger( WP_FS__SLUG, WP_FS__DEBUG_SDK, WP_FS__ECHO_DEBUG_SDK );
1870
1871 self::$_static_logger->entrance();
1872
1873 self::$_accounts = FS_Option_Manager::get_manager( WP_FS__ACCOUNTS_OPTION_NAME, true );
1874
1875 self::$_global_admin_notices = FS_Admin_Notice_Manager::instance( 'global' );
1876
1877 add_action( 'admin_menu', array( 'Freemius', '_add_debug_section' ) );
1878
1879 add_action( "wp_ajax_fs_toggle_debug_mode", array( 'Freemius', '_toggle_debug_mode' ) );
1880
1881 self::add_ajax_action_static( 'get_debug_log', array( 'Freemius', '_get_debug_log' ) );
1882
1883 self::add_ajax_action_static( 'get_db_option', array( 'Freemius', '_get_db_option' ) );
1884
1885 self::add_ajax_action_static( 'set_db_option', array( 'Freemius', '_set_db_option' ) );
1886
1887 if ( 0 == did_action( 'plugins_loaded' ) ) {
1888 add_action( 'plugins_loaded', array( 'Freemius', '_load_textdomain' ), 1 );
1889 }
1890
1891 self::$_statics_loaded = true;
1892 }
1893
1894 #----------------------------------------------------------------------------------
1895 #region Localization
1896 #----------------------------------------------------------------------------------
1897
1898 /**
1899 * Load framework's text domain.
1900 *
1901 * @author Vova Feldman (@svovaf)
1902 * @since 1.2.1
1903 */
1904 static function _load_textdomain() {
1905 if ( ! is_admin() ) {
1906 return;
1907 }
1908
1909 global $fs_active_plugins;
1910
1911 // Works both for plugins and themes.
1912 load_plugin_textdomain(
1913 'freemius',
1914 false,
1915 $fs_active_plugins->newest->sdk_path . '/languages/'
1916 );
1917 }
1918
1919 #endregion
1920
1921 #----------------------------------------------------------------------------------
1922 #region Debugging
1923 #----------------------------------------------------------------------------------
1924
1925 /**
1926 * @author Vova Feldman (@svovaf)
1927 * @since 1.0.8
1928 */
1929 static function _add_debug_section() {
1930 if ( ! current_user_can( 'activate_plugins' )
1931 && ! current_user_can( 'switch_themes' )
1932 ) {
1933 return;
1934 }
1935
1936 self::$_static_logger->entrance();
1937
1938 $title = sprintf( '%s [v.%s]', fs_text_inline( 'Freemius Debug' ), WP_FS__SDK_VERSION );
1939
1940 if ( WP_FS__DEV_MODE ) {
1941 // Add top-level debug menu item.
1942 $hook = FS_Admin_Menu_Manager::add_page(
1943 $title,
1944 $title,
1945 'manage_options',
1946 'freemius',
1947 array( 'Freemius', '_debug_page_render' )
1948 );
1949 } else {
1950 // Add hidden debug page.
1951 $hook = FS_Admin_Menu_Manager::add_subpage(
1952 null,
1953 $title,
1954 $title,
1955 'manage_options',
1956 'freemius',
1957 array( 'Freemius', '_debug_page_render' )
1958 );
1959 }
1960
1961 if ( ! empty( $hook ) ) {
1962 add_action( "load-$hook", array( 'Freemius', '_debug_page_actions' ) );
1963 }
1964 }
1965
1966 /**
1967 * @author Vova Feldman (@svovaf)
1968 * @since 1.1.7.3
1969 */
1970 static function _toggle_debug_mode() {
1971 $is_on = fs_request_get( 'is_on', false, 'post' );
1972
1973 if ( fs_request_is_post() && in_array( $is_on, array( 0, 1 ) ) ) {
1974 update_option( 'fs_debug_mode', $is_on );
1975
1976 // Turn on/off storage logging.
1977 FS_Logger::_set_storage_logging( ( 1 == $is_on ) );
1978 }
1979
1980 exit;
1981 }
1982
1983 /**
1984 * @author Vova Feldman (@svovaf)
1985 * @since 1.2.1.6
1986 */
1987 static function _get_debug_log() {
1988 $logs = FS_Logger::load_db_logs(
1989 fs_request_get( 'filters', false, 'post' ),
1990 ! empty( $_POST['limit'] ) && is_numeric( $_POST['limit'] ) ? $_POST['limit'] : 200,
1991 ! empty( $_POST['offset'] ) && is_numeric( $_POST['offset'] ) ? $_POST['offset'] : 0
1992 );
1993
1994 self::shoot_ajax_success( $logs );
1995 }
1996
1997 /**
1998 * @author Vova Feldman (@svovaf)
1999 * @since 1.2.1.7
2000 */
2001 static function _get_db_option() {
2002 $option_name = fs_request_get( 'option_name' );
2003
2004 $value = get_option( $option_name );
2005
2006 $result = array(
2007 'name' => $option_name,
2008 );
2009
2010 if ( false !== $value ) {
2011 if ( ! is_string( $value ) ) {
2012 $value = json_encode( $value );
2013 }
2014
2015 $result['value'] = $value;
2016 }
2017
2018 self::shoot_ajax_success( $result );
2019 }
2020
2021 /**
2022 * @author Vova Feldman (@svovaf)
2023 * @since 1.2.1.7
2024 */
2025 static function _set_db_option() {
2026 $option_name = fs_request_get( 'option_name' );
2027 $option_value = fs_request_get( 'option_value' );
2028
2029 if ( ! empty( $option_value ) ) {
2030 update_option( $option_name, $option_value );
2031 }
2032
2033 self::shoot_ajax_success();
2034 }
2035
2036 /**
2037 * @author Vova Feldman (@svovaf)
2038 * @since 1.0.8
2039 */
2040 static function _debug_page_actions() {
2041 self::_clean_admin_content_section();
2042
2043 if ( fs_request_is_action( 'restart_freemius' ) ) {
2044 check_admin_referer( 'restart_freemius' );
2045
2046 // Clear accounts data.
2047 self::$_accounts->clear( true );
2048
2049 // Clear SDK reference cache.
2050 delete_option( 'fs_active_plugins' );
2051 } else if ( fs_request_is_action( 'simulate_trial' ) ) {
2052 check_admin_referer( 'simulate_trial' );
2053
2054 $fs = freemius( fs_request_get( 'module_id' ) );
2055
2056 // Update SDK install to at least 24 hours before.
2057 $fs->_storage->install_timestamp = ( time() - WP_FS__TIME_24_HOURS_IN_SEC );
2058 // Unset the trial shown timestamp.
2059 unset( $fs->_storage->trial_promotion_shown );
2060 } else if ( fs_request_is_action( 'delete_install' ) ) {
2061 check_admin_referer( 'delete_install' );
2062
2063 self::_delete_site_by_slug(
2064 fs_request_get( 'slug' ),
2065 fs_request_get( 'module_type' )
2066 );
2067 } else if ( fs_request_is_action( 'download_logs' ) ) {
2068 check_admin_referer( 'download_logs' );
2069
2070 $download_url = FS_Logger::download_db_logs(
2071 fs_request_get( 'filters', false, 'post' )
2072 );
2073
2074 if ( false === $download_url ) {
2075 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.' );
2076 }
2077
2078 fs_redirect( $download_url );
2079 }
2080 }
2081
2082 /**
2083 * @author Vova Feldman (@svovaf)
2084 * @since 1.0.8
2085 */
2086 static function _debug_page_render() {
2087 self::$_static_logger->entrance();
2088
2089 $vars = array(
2090 'plugin_sites' => self::get_all_sites(),
2091 'theme_sites' => self::get_all_sites( WP_FS__MODULE_TYPE_THEME ),
2092 'users' => self::get_all_users(),
2093 'addons' => self::get_all_addons(),
2094 'account_addons' => self::get_all_account_addons(),
2095 'plugin_licenses' => self::get_all_licenses(),
2096 'theme_licenses' => self::get_all_licenses( WP_FS__MODULE_TYPE_THEME )
2097 );
2098
2099 fs_enqueue_local_style( 'fs_debug', '/admin/debug.css' );
2100 fs_require_once_template( 'debug.php', $vars );
2101 }
2102
2103 #endregion
2104
2105 #----------------------------------------------------------------------------------
2106 #region Connectivity Issues
2107 #----------------------------------------------------------------------------------
2108
2109 /**
2110 * Check if Freemius should be turned on for the current plugin install.
2111 *
2112 * Note:
2113 * $this->_is_on is updated in has_api_connectivity()
2114 *
2115 * @author Vova Feldman (@svovaf)
2116 * @since 1.0.9
2117 *
2118 * @return bool
2119 */
2120 function is_on() {
2121 self::$_static_logger->entrance();
2122
2123 if ( isset( $this->_is_on ) ) {
2124 return $this->_is_on;
2125 }
2126
2127 // If already installed or pending then sure it's on :)
2128 if ( $this->is_registered() || $this->is_pending_activation() ) {
2129 $this->_is_on = true;
2130
2131 return true;
2132 }
2133
2134 return false;
2135 }
2136
2137 /**
2138 * @author Vova Feldman (@svovaf)
2139 * @since 1.1.7.3
2140 *
2141 * @param bool $flush_if_no_connectivity
2142 *
2143 * @return bool
2144 */
2145 private function should_run_connectivity_test( $flush_if_no_connectivity = false ) {
2146 if ( ! isset( $this->_storage->connectivity_test ) ) {
2147 // Connectivity test was never executed, or cache was cleared.
2148 return true;
2149 }
2150
2151 if ( WP_FS__PING_API_ON_IP_OR_HOST_CHANGES ) {
2152 if ( WP_FS__IS_HTTP_REQUEST ) {
2153 if ( $_SERVER['HTTP_HOST'] != $this->_storage->connectivity_test['host'] ) {
2154 // Domain changed.
2155 return true;
2156 }
2157
2158 if ( WP_FS__REMOTE_ADDR != $this->_storage->connectivity_test['server_ip'] ) {
2159 // Server IP changed.
2160 return true;
2161 }
2162 }
2163 }
2164
2165 if ( $this->_storage->connectivity_test['is_connected'] &&
2166 $this->_storage->connectivity_test['is_active']
2167 ) {
2168 // API connected and Freemius is active - no need to run connectivity check.
2169 return false;
2170 }
2171
2172 if ( $flush_if_no_connectivity ) {
2173 /**
2174 * If explicitly asked to flush when no connectivity - do it only
2175 * if at least 10 sec passed from the last API connectivity test.
2176 */
2177 return ( isset( $this->_storage->connectivity_test['timestamp'] ) &&
2178 ( WP_FS__SCRIPT_START_TIME - $this->_storage->connectivity_test['timestamp'] ) > 10 );
2179 }
2180
2181 /**
2182 * @since 1.1.7 Don't check for connectivity on plugin downgrade.
2183 */
2184 $version = $this->get_plugin_version();
2185 if ( version_compare( $version, $this->_storage->connectivity_test['version'], '>' ) ) {
2186 // If it's a plugin version upgrade and Freemius is off or no connectivity, run connectivity test.
2187 return true;
2188 }
2189
2190 return false;
2191 }
2192
2193 /**
2194 * @author Vova Feldman (@svovaf)
2195 * @since 1.1.7.4
2196 *
2197 * @return object|false
2198 */
2199 private function ping() {
2200 if ( WP_FS__SIMULATE_NO_API_CONNECTIVITY ) {
2201 return false;
2202 }
2203
2204 $version = $this->get_plugin_version();
2205
2206 $is_update = $this->apply_filters( 'is_plugin_update', $this->is_plugin_update() );
2207
2208 return $this->get_api_plugin_scope()->ping(
2209 $this->get_anonymous_id(),
2210 array(
2211 'is_update' => json_encode( $is_update ),
2212 'version' => $version,
2213 'sdk' => $this->version,
2214 'is_admin' => json_encode( is_admin() ),
2215 'is_ajax' => json_encode( self::is_ajax() ),
2216 'is_cron' => json_encode( self::is_cron() ),
2217 'is_http' => json_encode( WP_FS__IS_HTTP_REQUEST ),
2218 )
2219 );
2220 }
2221
2222 /**
2223 * Check if there's any connectivity issue to Freemius API.
2224 *
2225 * @author Vova Feldman (@svovaf)
2226 * @since 1.0.9
2227 *
2228 * @param bool $flush_if_no_connectivity
2229 *
2230 * @return bool
2231 */
2232 function has_api_connectivity( $flush_if_no_connectivity = false ) {
2233 $this->_logger->entrance();
2234
2235 if ( isset( $this->_has_api_connection ) && ( $this->_has_api_connection || ! $flush_if_no_connectivity ) ) {
2236 return $this->_has_api_connection;
2237 }
2238
2239 if ( WP_FS__SIMULATE_NO_API_CONNECTIVITY &&
2240 isset( $this->_storage->connectivity_test ) &&
2241 true === $this->_storage->connectivity_test['is_connected']
2242 ) {
2243 unset( $this->_storage->connectivity_test );
2244 }
2245
2246 if ( ! $this->should_run_connectivity_test( $flush_if_no_connectivity ) ) {
2247 $this->_has_api_connection = $this->_storage->connectivity_test['is_connected'];
2248 /**
2249 * @since 1.1.6 During dev mode, if there's connectivity - turn Freemius on regardless the configuration.
2250 *
2251 * @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.
2252 */
2253 $this->_is_on = $this->_storage->connectivity_test['is_active'] ||
2254 $this->is_premium() ||
2255 ( WP_FS__DEV_MODE && $this->_has_api_connection && ! WP_FS__SIMULATE_FREEMIUS_OFF );
2256
2257 return $this->_has_api_connection;
2258 }
2259
2260 $pong = $this->ping();
2261 $is_connected = $this->get_api_plugin_scope()->is_valid_ping( $pong );
2262
2263 if ( ! $is_connected ) {
2264 // API failure.
2265 $this->_add_connectivity_issue_message( $pong );
2266 }
2267
2268 $this->store_connectivity_info( $pong, $is_connected );
2269
2270 return $this->_has_api_connection;
2271 }
2272
2273 /**
2274 * @author Vova Feldman (@svovaf)
2275 * @since 1.1.7.4
2276 *
2277 * @param object $pong
2278 * @param bool $is_connected
2279 */
2280 private function store_connectivity_info( $pong, $is_connected ) {
2281 $this->_logger->entrance();
2282
2283 $version = $this->get_plugin_version();
2284
2285 if ( ! $is_connected || WP_FS__SIMULATE_FREEMIUS_OFF ) {
2286 $is_active = false;
2287 } else {
2288 $is_active = ( isset( $pong->is_active ) && true == $pong->is_active );
2289 }
2290
2291 $is_active = $this->apply_filters(
2292 'is_on',
2293 $is_active,
2294 $this->is_plugin_update(),
2295 $version
2296 );
2297
2298 $this->_storage->connectivity_test = array(
2299 'is_connected' => $is_connected,
2300 'host' => $_SERVER['HTTP_HOST'],
2301 'server_ip' => WP_FS__REMOTE_ADDR,
2302 'is_active' => $is_active,
2303 'timestamp' => WP_FS__SCRIPT_START_TIME,
2304 // Last version with connectivity attempt.
2305 'version' => $version,
2306 );
2307
2308 $this->_has_api_connection = $is_connected;
2309 $this->_is_on = $is_active || ( WP_FS__DEV_MODE && $is_connected && ! WP_FS__SIMULATE_FREEMIUS_OFF );
2310 }
2311
2312 /**
2313 * Force turning Freemius on.
2314 *
2315 * @author Vova Feldman (@svovaf)
2316 * @since 1.1.8.1
2317 *
2318 * @return bool TRUE if successfully turned on.
2319 */
2320 private function turn_on() {
2321 $this->_logger->entrance();
2322
2323 if ( $this->is_on() || ! isset( $this->_storage->connectivity_test['is_active'] ) ) {
2324 return false;
2325 }
2326
2327 $updated_connectivity = $this->_storage->connectivity_test;
2328 $updated_connectivity['is_active'] = true;
2329 $updated_connectivity['timestamp'] = WP_FS__SCRIPT_START_TIME;
2330 $this->_storage->connectivity_test = $updated_connectivity;
2331
2332 $this->_is_on = true;
2333
2334 return true;
2335 }
2336
2337 /**
2338 * Anonymous and unique site identifier (Hash).
2339 *
2340 * @author Vova Feldman (@svovaf)
2341 * @since 1.1.0
2342 *
2343 * @return string
2344 */
2345 function get_anonymous_id() {
2346 $unique_id = self::$_accounts->get_option( 'unique_id' );
2347
2348 if ( empty( $unique_id ) || ! is_string( $unique_id ) ) {
2349 $key = get_site_url();
2350
2351 // If localhost, assign microtime instead of domain.
2352 if ( WP_FS__IS_LOCALHOST ||
2353 false !== strpos( $key, 'localhost' ) ||
2354 false === strpos( $key, '.' )
2355 ) {
2356 $key = microtime();
2357 }
2358
2359 /**
2360 * Base the unique identifier on the WP secure authentication key. Which
2361 * turns the key into a secret anonymous identifier.
2362 *
2363 * @author Vova Feldman (@svovaf)
2364 * @since 1.2.3
2365 */
2366 $unique_id = md5( $key . SECURE_AUTH_KEY );
2367
2368 self::$_accounts->set_option( 'unique_id', $unique_id, true );
2369 }
2370
2371 $this->_logger->departure( $unique_id );
2372
2373 return $unique_id;
2374 }
2375
2376 /**
2377 * @author Vova Feldman (@svovaf)
2378 * @since 1.1.7.4
2379 *
2380 * @return \WP_User
2381 */
2382 static function _get_current_wp_user() {
2383 self::require_pluggable_essentials();
2384
2385 return wp_get_current_user();
2386 }
2387
2388 /**
2389 * @author Vova Feldman (@svovaf)
2390 * @since 1.2.1.7
2391 *
2392 * @param string $email
2393 *
2394 * @return bool
2395 */
2396 static function is_valid_email( $email ) {
2397 if ( false === filter_var( $email, FILTER_VALIDATE_EMAIL ) ) {
2398 return false;
2399 }
2400
2401 $parts = explode( '@', $email );
2402
2403 if ( 2 !== count( $parts ) || empty( $parts[1] ) ) {
2404 return false;
2405 }
2406
2407 $blacklist = array(
2408 'admin.',
2409 'webmaster.',
2410 'localhost.',
2411 'dev.',
2412 'development.',
2413 'test.',
2414 'stage.',
2415 'staging.',
2416 );
2417
2418 // Make sure domain is not one of the blacklisted.
2419 foreach ( $blacklist as $invalid ) {
2420 if ( 0 === strpos( $parts[1], $invalid ) ) {
2421 return false;
2422 }
2423 }
2424
2425 // Get the UTF encoded domain name.
2426 $domain = idn_to_ascii( $parts[1] ) . '.';
2427
2428 return ( checkdnsrr( $domain, 'MX' ) || checkdnsrr( $domain, 'A' ) );
2429 }
2430
2431 /**
2432 * Generate API connectivity issue message.
2433 *
2434 * @author Vova Feldman (@svovaf)
2435 * @since 1.0.9
2436 *
2437 * @param mixed $api_result
2438 * @param bool $is_first_failure
2439 */
2440 function _add_connectivity_issue_message( $api_result, $is_first_failure = true ) {
2441 if ( ! $this->is_premium() && $this->_enable_anonymous ) {
2442 // Don't add message if it's the free version and can run anonymously.
2443 return;
2444 }
2445
2446 if ( ! function_exists( 'wp_nonce_url' ) ) {
2447 require_once ABSPATH . 'wp-includes/functions.php';
2448 }
2449
2450 $current_user = self::_get_current_wp_user();
2451 // $admin_email = get_option( 'admin_email' );
2452 $admin_email = $current_user->user_email;
2453
2454 // Aliases.
2455 $deactivate_plugin_title = $this->esc_html_inline( 'That\'s exhausting, please deactivate', 'deactivate-plugin-title' );
2456 $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' );
2457 $install_previous_title = $this->esc_html_inline( 'Let\'s try your previous version', 'install-previous-title' );
2458 $install_previous_desc = $this->esc_html_inline( 'Uninstall this version and install the previous one.', 'install-previous-desc' );
2459 $fix_issue_title = $this->esc_html_inline( 'Yes - I\'m giving you a chance to fix it', 'fix-issue-title' );
2460 $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' );
2461 /* translators: %s: product title (e.g. "Awesome Plugin" requires an access to...) */
2462 $x_requires_access_to_api = $this->esc_html_inline( '%s requires an access to our API.', 'x-requires-access-to-api' );
2463 $sysadmin_title = $this->esc_html_inline( 'I\'m a system administrator', 'sysadmin-title' );
2464 $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' );
2465
2466 $message = false;
2467 if ( is_object( $api_result ) &&
2468 isset( $api_result->error ) &&
2469 isset( $api_result->error->code )
2470 ) {
2471 switch ( $api_result->error->code ) {
2472 case 'curl_missing':
2473 $missing_methods = '';
2474 if ( is_array( $api_result->missing_methods ) &&
2475 ! empty( $api_result->missing_methods )
2476 ) {
2477 foreach ( $api_result->missing_methods as $m ) {
2478 if ( 'curl_version' === $m ) {
2479 continue;
2480 }
2481
2482 if ( ! empty( $missing_methods ) ) {
2483 $missing_methods .= ', ';
2484 }
2485
2486 $missing_methods .= sprintf( '<code>%s</code>', $m );
2487 }
2488
2489 if ( ! empty( $missing_methods ) ) {
2490 $missing_methods = sprintf(
2491 '<br><br><b>%s</b> %s',
2492 $this->esc_html_inline( 'Disabled method(s):', 'curl-disabled-methods' ),
2493 $missing_methods
2494 );
2495 }
2496 }
2497
2498 $message = sprintf(
2499 $x_requires_access_to_api . ' ' .
2500 $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' ) . ' ' .
2501 $missing_methods .
2502 ' %s',
2503 '<b>' . $this->get_plugin_name() . '</b>',
2504 sprintf(
2505 '<ol id="fs_firewall_issue_options"><li>%s</li><li>%s</li><li>%s</li></ol>',
2506 sprintf(
2507 '<a class="fs-resolve" data-type="curl" href="#"><b>%s</b></a>%s',
2508 $this->get_text_inline( 'I don\'t know what is cURL or how to install it, help me!', 'curl-missing-no-clue-title' ),
2509 ' - ' . sprintf(
2510 $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' ),
2511 '<a href="mailto:' . $admin_email . '">' . $admin_email . '</a>'
2512 )
2513 ),
2514 sprintf(
2515 '<b>%s</b> - %s',
2516 $sysadmin_title,
2517 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 ) ) )
2518 ),
2519 sprintf(
2520 '<a href="%s"><b>%s</b></a> - %s',
2521 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 ),
2522 $deactivate_plugin_title,
2523 $deactivate_plugin_desc
2524 )
2525 )
2526 );
2527 break;
2528 case 'cloudflare_ddos_protection':
2529 $message = sprintf(
2530 $x_requires_access_to_api . ' ' .
2531 $this->esc_html_inline( 'From unknown reason, CloudFlare, the firewall we use, blocks the connection.', 'cloudflare-blocks-connection-message' ) . ' ' .
2532 $happy_to_resolve_issue_asap .
2533 ' %s',
2534 '<b>' . $this->get_plugin_name() . '</b>',
2535 sprintf(
2536 '<ol id="fs_firewall_issue_options"><li>%s</li><li>%s</li><li>%s</li></ol>',
2537 sprintf(
2538 '<a class="fs-resolve" data-type="cloudflare" href="#"><b>%s</b></a>%s',
2539 $fix_issue_title,
2540 ' - ' . sprintf(
2541 $fix_issue_desc,
2542 '<a href="mailto:' . $admin_email . '">' . $admin_email . '</a>'
2543 )
2544 ),
2545 sprintf(
2546 '<a href="%s" target="_blank"><b>%s</b></a> - %s',
2547 sprintf( 'https://wordpress.org/plugins/%s/download/', $this->_slug ),
2548 $install_previous_title,
2549 $install_previous_desc
2550 ),
2551 sprintf(
2552 '<a href="%s"><b>%s</b></a> - %s',
2553 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 ),
2554 $deactivate_plugin_title,
2555 $deactivate_plugin_desc
2556 )
2557 )
2558 );
2559 break;
2560 case 'squid_cache_block':
2561 $message = sprintf(
2562 $x_requires_access_to_api . ' ' .
2563 $this->esc_html_inline( 'It looks like your server is using Squid ACL (access control lists), which blocks the connection.', 'squid-blocks-connection-message' ) .
2564 ' %s',
2565 '<b>' . $this->get_plugin_name() . '</b>',
2566 sprintf(
2567 '<ol id="fs_firewall_issue_options"><li>%s</li><li>%s</li><li>%s</li></ol>',
2568 sprintf(
2569 '<a class="fs-resolve" data-type="squid" href="#"><b>%s</b></a> - %s',
2570 $this->esc_html_inline( 'I don\'t know what is Squid or ACL, help me!', 'squid-no-clue-title' ),
2571 sprintf(
2572 $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' ),
2573 '<a href="mailto:' . $admin_email . '">' . $admin_email . '</a>'
2574 )
2575 ),
2576 sprintf(
2577 '<b>%s</b> - %s',
2578 $sysadmin_title,
2579 sprintf(
2580 $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' ),
2581 // We use a filter since the plugin might require additional API connectivity.
2582 '<b>' . implode( ', ', $this->apply_filters( 'api_domains', array( 'api.freemius.com', 'wp.freemius.com' ) ) ) . '</b>',
2583 $this->_module_type
2584 )
2585 ),
2586 sprintf(
2587 '<a href="%s"><b>%s</b></a> - %s',
2588 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 ),
2589 $deactivate_plugin_title,
2590 $deactivate_plugin_desc
2591 )
2592 )
2593 );
2594 break;
2595 // default:
2596 // $message = $this->get_text_inline( 'connectivity-test-fails-message' );
2597 // break;
2598 }
2599 }
2600
2601 $message_id = 'failed_connect_api';
2602 $type = 'error';
2603
2604 $connectivity_test_fails_message = $this->esc_html_inline( 'From unknown reason, the API connectivity test failed.', 'connectivity-test-fails-message' );
2605
2606 if ( false === $message ) {
2607 if ( $is_first_failure ) {
2608 // First attempt failed.
2609 $message = sprintf(
2610 $x_requires_access_to_api . ' ' .
2611 $connectivity_test_fails_message . ' ' .
2612 $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>' .
2613 '%s',
2614 '<b>' . $this->get_plugin_name() . '</b>',
2615 sprintf(
2616 '<div id="fs_firewall_issue_options">%s %s</div>',
2617 sprintf(
2618 '<a class="button button-primary fs-resolve" data-type="retry_ping" href="#">%s</a>',
2619 $this->get_text_inline( 'Yes - do your thing', 'yes-do-your-thing' )
2620 ),
2621 sprintf(
2622 '<a href="%s" class="button">%s</a>',
2623 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 ),
2624 $this->get_text_inline( 'No - just deactivate', 'no-deactivate' )
2625 )
2626 )
2627 );
2628
2629 $message_id = 'failed_connect_api_first';
2630 $type = 'promotion';
2631 } else {
2632 // Second connectivity attempt failed.
2633 $message = sprintf(
2634 $x_requires_access_to_api . ' ' .
2635 $connectivity_test_fails_message . ' ' .
2636 $happy_to_resolve_issue_asap .
2637 ' %s',
2638 '<b>' . $this->get_plugin_name() . '</b>',
2639 sprintf(
2640 '<ol id="fs_firewall_issue_options"><li>%s</li><li>%s</li><li>%s</li></ol>',
2641 sprintf(
2642 '<a class="fs-resolve" data-type="general" href="#"><b>%s</b></a>%s',
2643 $fix_issue_title,
2644 ' - ' . sprintf(
2645 $fix_issue_desc,
2646 '<a href="mailto:' . $admin_email . '">' . $admin_email . '</a>'
2647 )
2648 ),
2649 sprintf(
2650 '<a href="%s" target="_blank"><b>%s</b></a> - %s',
2651 sprintf( 'https://wordpress.org/plugins/%s/download/', $this->_slug ),
2652 $install_previous_title,
2653 $install_previous_desc
2654 ),
2655 sprintf(
2656 '<a href="%s"><b>%s</b></a> - %s',
2657 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 ),
2658 $deactivate_plugin_title,
2659 $deactivate_plugin_desc
2660 )
2661 )
2662 );
2663 }
2664 }
2665
2666 $this->_admin_notices->add_sticky(
2667 $message,
2668 $message_id,
2669 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
2670 $type
2671 );
2672 }
2673
2674 /**
2675 * Handle user request to resolve connectivity issue.
2676 * This method will send an email to Freemius API technical staff for resolution.
2677 * The email will contain server's info and installed plugins (might be caching issue).
2678 *
2679 * @author Vova Feldman (@svovaf)
2680 * @since 1.0.9
2681 */
2682 function _email_about_firewall_issue() {
2683 $this->_admin_notices->remove_sticky( 'failed_connect_api' );
2684
2685 $pong = $this->ping();
2686
2687 $is_connected = $this->get_api_plugin_scope()->is_valid_ping( $pong );
2688
2689 if ( $is_connected ) {
2690 $this->store_connectivity_info( $pong, $is_connected );
2691
2692 echo $this->get_after_plugin_activation_redirect_url();
2693 exit;
2694 }
2695
2696 $current_user = self::_get_current_wp_user();
2697 $admin_email = $current_user->user_email;
2698
2699 $error_type = fs_request_get( 'error_type', 'general' );
2700
2701 switch ( $error_type ) {
2702 case 'squid':
2703 $title = 'Squid ACL Blocking Issue';
2704 break;
2705 case 'cloudflare':
2706 $title = 'CloudFlare Blocking Issue';
2707 break;
2708 default:
2709 $title = 'API Connectivity Issue';
2710 break;
2711 }
2712
2713 $custom_email_sections = array();
2714
2715 // Add 'API Error' custom email section.
2716 $custom_email_sections['api_error'] = array(
2717 'title' => 'API Error',
2718 'rows' => array(
2719 'ping' => array(
2720 'API Error',
2721 is_string( $pong ) ? htmlentities( $pong ) : json_encode( $pong )
2722 ),
2723 )
2724 );
2725
2726 // Send email with technical details to resolve API connectivity issues.
2727 $this->send_email(
2728 'api@freemius.com', // recipient
2729 $title . ' [' . $this->get_plugin_name() . ']', // subject
2730 $custom_email_sections,
2731 array( "Reply-To: $admin_email <$admin_email>" ) // headers
2732 );
2733
2734 $this->_admin_notices->add_sticky(
2735 sprintf(
2736 $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' ),
2737 '<a href="mailto:' . $admin_email . '">' . $admin_email . '</a>'
2738 ),
2739 'server_details_sent'
2740 );
2741
2742 // Action was taken, tell that API connectivity troubleshooting should be off now.
2743
2744 echo "1";
2745 exit;
2746 }
2747
2748 /**
2749 * Handle connectivity test retry approved by the user.
2750 *
2751 * @author Vova Feldman (@svovaf)
2752 * @since 1.1.7.4
2753 */
2754 function _retry_connectivity_test() {
2755 $this->_admin_notices->remove_sticky( 'failed_connect_api_first' );
2756
2757 $pong = $this->ping();
2758
2759 $is_connected = $this->get_api_plugin_scope()->is_valid_ping( $pong );
2760
2761 if ( $is_connected ) {
2762 $this->store_connectivity_info( $pong, $is_connected );
2763
2764 echo $this->get_after_plugin_activation_redirect_url();
2765 } else {
2766 // Add connectivity issue message after 2nd failed attempt.
2767 $this->_add_connectivity_issue_message( $pong, false );
2768
2769 echo "1";
2770 }
2771
2772 exit;
2773 }
2774
2775 static function _add_firewall_issues_javascript() {
2776 $params = array();
2777 fs_require_once_template( 'firewall-issues-js.php', $params );
2778 }
2779
2780 #endregion
2781
2782 #----------------------------------------------------------------------------------
2783 #region Email
2784 #----------------------------------------------------------------------------------
2785
2786 /**
2787 * Generates and sends an HTML email with customizable sections.
2788 *
2789 * @author Leo Fajardo (@leorw)
2790 * @since 1.1.2
2791 *
2792 * @param string $to_address
2793 * @param string $subject
2794 * @param array $sections
2795 * @param array $headers
2796 *
2797 * @return bool Whether the email contents were sent successfully.
2798 */
2799 private function send_email(
2800 $to_address,
2801 $subject,
2802 $sections = array(),
2803 $headers = array()
2804 ) {
2805 $default_sections = $this->get_email_sections();
2806
2807 // Insert new sections or replace the default email sections.
2808 if ( is_array( $sections ) && ! empty( $sections ) ) {
2809 foreach ( $sections as $section_id => $custom_section ) {
2810 if ( ! isset( $default_sections[ $section_id ] ) ) {
2811 // If the section does not exist, add it.
2812 $default_sections[ $section_id ] = $custom_section;
2813 } else {
2814 // If the section already exists, override it.
2815 $current_section = $default_sections[ $section_id ];
2816
2817 // Replace the current section's title if a custom section title exists.
2818 if ( isset( $custom_section['title'] ) ) {
2819 $current_section['title'] = $custom_section['title'];
2820 }
2821
2822 // Insert new rows under the current section or replace the default rows.
2823 if ( isset( $custom_section['rows'] ) && is_array( $custom_section['rows'] ) && ! empty( $custom_section['rows'] ) ) {
2824 foreach ( $custom_section['rows'] as $row_id => $row ) {
2825 $current_section['rows'][ $row_id ] = $row;
2826 }
2827 }
2828
2829 $default_sections[ $section_id ] = $current_section;
2830 }
2831 }
2832 }
2833
2834 $vars = array( 'sections' => $default_sections );
2835 $message = fs_get_template( 'email.php', $vars );
2836
2837 // Set the type of email to HTML.
2838 $headers[] = 'Content-type: text/html; charset=UTF-8';
2839
2840 $header_string = implode( "\r\n", $headers );
2841
2842 return wp_mail(
2843 $to_address,
2844 $subject,
2845 $message,
2846 $header_string
2847 );
2848 }
2849
2850 /**
2851 * Generates the data for the sections of the email content.
2852 *
2853 * @author Leo Fajardo (@leorw)
2854 * @since 1.1.2
2855 *
2856 * @return array
2857 */
2858 private function get_email_sections() {
2859 // Retrieve the current user's information so that we can get the user's email, first name, and last name below.
2860 $current_user = self::_get_current_wp_user();
2861
2862 // Retrieve the cURL version information so that we can get the version number below.
2863 $curl_version_information = curl_version();
2864
2865 $active_plugin = self::get_active_plugins();
2866
2867 // Generate the list of active plugins separated by new line.
2868 $active_plugin_string = '';
2869 foreach ( $active_plugin as $plugin ) {
2870 $active_plugin_string .= sprintf(
2871 '<a href="%s">%s</a> [v%s]<br>',
2872 $plugin['PluginURI'],
2873 $plugin['Name'],
2874 $plugin['Version']
2875 );
2876 }
2877
2878 $server_ip = WP_FS__REMOTE_ADDR;
2879
2880 // Add PHP info for deeper investigation.
2881 ob_start();
2882 phpinfo();
2883 $php_info = ob_get_clean();
2884
2885 $api_domain = substr( FS_API__ADDRESS, strpos( FS_API__ADDRESS, ':' ) + 3 );
2886
2887 // Generate the default email sections.
2888 $sections = array(
2889 'sdk' => array(
2890 'title' => 'SDK',
2891 'rows' => array(
2892 'fs_version' => array( 'FS Version', $this->version ),
2893 'curl_version' => array( 'cURL Version', $curl_version_information['version'] )
2894 )
2895 ),
2896 'plugin' => array(
2897 'title' => ucfirst( $this->get_module_type() ),
2898 'rows' => array(
2899 'name' => array( 'Name', $this->get_plugin_name() ),
2900 'version' => array( 'Version', $this->get_plugin_version() )
2901 )
2902 ),
2903 'api' => array(
2904 'title' => 'API Subdomain',
2905 'rows' => array(
2906 'dns' => array(
2907 'DNS_CNAME',
2908 function_exists( 'dns_get_record' ) ?
2909 var_export( dns_get_record( $api_domain, DNS_CNAME ), true ) :
2910 'dns_get_record() disabled/blocked'
2911 ),
2912 'ip' => array(
2913 'IP',
2914 function_exists( 'gethostbyname' ) ?
2915 gethostbyname( $api_domain ) :
2916 'gethostbyname() disabled/blocked'
2917 ),
2918 ),
2919 ),
2920 'site' => array(
2921 'title' => 'Site',
2922 'rows' => array(
2923 'unique_id' => array( 'Unique ID', $this->get_anonymous_id() ),
2924 'address' => array( 'Address', site_url() ),
2925 'host' => array(
2926 'HTTP_HOST',
2927 ( ! empty( $_SERVER['HTTP_HOST'] ) ? $_SERVER['HTTP_HOST'] : '' )
2928 ),
2929 'hosting' => array(
2930 'Hosting Company' => fs_request_has( 'hosting_company' ) ?
2931 fs_request_get( 'hosting_company' ) :
2932 'Unknown',
2933 ),
2934 'server_addr' => array(
2935 'SERVER_ADDR',
2936 '<a href="http://www.projecthoneypot.org/ip_' . $server_ip . '">' . $server_ip . '</a>'
2937 )
2938 )
2939 ),
2940 'user' => array(
2941 'title' => 'User',
2942 'rows' => array(
2943 'email' => array( 'Email', $current_user->user_email ),
2944 'first' => array( 'First', $current_user->user_firstname ),
2945 'last' => array( 'Last', $current_user->user_lastname )
2946 )
2947 ),
2948 'plugins' => array(
2949 'title' => 'Plugins',
2950 'rows' => array(
2951 'active_plugins' => array( 'Active Plugins', $active_plugin_string )
2952 )
2953 ),
2954 'php_info' => array(
2955 'title' => 'PHP Info',
2956 'rows' => array(
2957 'info' => array( $php_info )
2958 ),
2959 )
2960 );
2961
2962 // Allow the sections to be modified by other code.
2963 $sections = $this->apply_filters( 'email_template_sections', $sections );
2964
2965 return $sections;
2966 }
2967
2968 #endregion
2969
2970 #----------------------------------------------------------------------------------
2971 #region Initialization
2972 #----------------------------------------------------------------------------------
2973
2974 /**
2975 * Init plugin's Freemius instance.
2976 *
2977 * @author Vova Feldman (@svovaf)
2978 * @since 1.0.1
2979 *
2980 * @param number $id
2981 * @param string $public_key
2982 * @param bool $is_live
2983 * @param bool $is_premium
2984 */
2985 function init( $id, $public_key, $is_live = true, $is_premium = true ) {
2986 $this->_logger->entrance();
2987
2988 $this->dynamic_init( array(
2989 'id' => $id,
2990 'public_key' => $public_key,
2991 'is_live' => $is_live,
2992 'is_premium' => $is_premium,
2993 ) );
2994 }
2995
2996 /**
2997 * Dynamic initiator, originally created to support initiation
2998 * with parent_id for add-ons.
2999 *
3000 * @author Vova Feldman (@svovaf)
3001 * @since 1.0.6
3002 *
3003 * @param array $plugin_info
3004 *
3005 * @throws Freemius_Exception
3006 */
3007 function dynamic_init( array $plugin_info ) {
3008 $this->_logger->entrance();
3009
3010 $this->parse_settings( $plugin_info );
3011
3012 if ( ! self::is_ajax() ) {
3013 if ( ! $this->is_addon() || $this->is_only_premium() ) {
3014 add_action( 'admin_menu', array( &$this, '_prepare_admin_menu' ), WP_FS__LOWEST_PRIORITY );
3015 }
3016 }
3017
3018 if ( $this->should_stop_execution() ) {
3019 return;
3020 }
3021
3022 if ( ! $this->is_registered() ) {
3023 if ( $this->is_anonymous() ) {
3024 // If user skipped, no need to test connectivity.
3025 $this->_has_api_connection = true;
3026 $this->_is_on = true;
3027 } else {
3028 if ( ! $this->has_api_connectivity() ) {
3029 if ( $this->_admin_notices->has_sticky( 'failed_connect_api_first' ) ||
3030 $this->_admin_notices->has_sticky( 'failed_connect_api' )
3031 ) {
3032 if ( ! $this->_enable_anonymous || $this->is_premium() ) {
3033 // If anonymous mode is disabled, add firewall admin-notice message.
3034 add_action( 'admin_footer', array( 'Freemius', '_add_firewall_issues_javascript' ) );
3035
3036 $this->add_ajax_action( 'resolve_firewall_issues', array(
3037 &$this,
3038 '_email_about_firewall_issue'
3039 ) );
3040
3041 $this->add_ajax_action( 'retry_connectivity_test', array(
3042 &$this,
3043 '_retry_connectivity_test'
3044 ) );
3045 }
3046 }
3047
3048 return;
3049 } else {
3050 $this->_admin_notices->remove_sticky( array(
3051 'failed_connect_api_first',
3052 'failed_connect_api',
3053 ) );
3054
3055 if ( $this->_anonymous_mode ) {
3056 // Simulate anonymous mode.
3057 $this->_is_anonymous = true;
3058 }
3059 }
3060 }
3061
3062 // Check if Freemius is on for the current plugin.
3063 // This MUST be executed after all the plugin variables has been loaded.
3064 if ( ! $this->is_on() ) {
3065 return;
3066 }
3067 }
3068
3069 if ( $this->has_api_connectivity() ) {
3070 if ( self::is_cron() ) {
3071 $this->hook_callback_to_sync_cron();
3072 } else if ( $this->is_user_in_admin() ) {
3073 /**
3074 * Schedule daily data sync cron if:
3075 *
3076 * 1. User opted-in (for tracking).
3077 * 2. If skipped, but later upgraded (opted-in via upgrade).
3078 *
3079 * @author Vova Feldman (@svovaf)
3080 * @since 1.1.7.3
3081 *
3082 */
3083 if ( $this->is_registered() ) {
3084 if ( ! $this->is_sync_cron_on() && $this->is_tracking_allowed() ) {
3085 $this->schedule_sync_cron();
3086 }
3087 }
3088
3089 /**
3090 * Check if requested for manual blocking background sync.
3091 */
3092 if ( fs_request_has( 'background_sync' ) ) {
3093 $this->run_manual_sync();
3094 }
3095 }
3096 }
3097
3098 if ( $this->is_registered() ) {
3099 $this->hook_callback_to_install_sync();
3100 }
3101
3102 if ( $this->is_addon() ) {
3103 if ( $this->is_parent_plugin_installed() ) {
3104 // Link to parent FS.
3105 $this->_parent = self::get_instance_by_id( $this->_plugin->parent_plugin_id );
3106
3107 // Get parent plugin reference.
3108 $this->_parent_plugin = $this->_parent->get_plugin();
3109 }
3110 }
3111
3112 if ( $this->is_user_in_admin() ) {
3113 if ( self::is_plugins_page() && $this->is_plugin() ) {
3114 $this->hook_plugin_action_links();
3115 }
3116
3117 if ( $this->is_addon() ) {
3118 if ( ! $this->is_parent_plugin_installed() ) {
3119 $parent_name = $this->get_option( $plugin_info, 'parent_name', null );
3120
3121 if ( isset( $plugin_info['parent'] ) ) {
3122 $parent_name = $this->get_option( $plugin_info['parent'], 'name', null );
3123 }
3124
3125 $this->_admin_notices->add(
3126 ( ! empty( $parent_name ) ?
3127 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 ) :
3128 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() )
3129 ),
3130 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
3131 'error'
3132 );
3133
3134 return;
3135 } else {
3136 if ( $this->_parent->is_registered() && ! $this->is_registered() ) {
3137 // If parent plugin activated, automatically install add-on for the user.
3138 $this->_activate_addon_account( $this->_parent );
3139 } else if ( ! $this->_parent->is_registered() && $this->is_registered() ) {
3140 // If add-on activated and parent not, automatically install parent for the user.
3141 $this->activate_parent_account( $this->_parent );
3142 }
3143
3144 // @todo This should be only executed on activation. It should be migrated to register_activation_hook() together with other activation related logic.
3145 if ( $this->is_premium() ) {
3146 // Remove add-on download admin-notice.
3147 $this->_parent->_admin_notices->remove_sticky( array(
3148 'addon_plan_upgraded_' . $this->_slug,
3149 'no_addon_license_' . $this->_slug,
3150 ) );
3151 }
3152
3153 // $this->deactivate_premium_only_addon_without_license();
3154 }
3155 } else {
3156 if ( $this->has_addons() &&
3157 'plugin-information' === fs_request_get( 'tab', false ) &&
3158 $this->get_id() == fs_request_get( 'parent_plugin_id', false )
3159 ) {
3160 require_once WP_FS__DIR_INCLUDES . '/fs-plugin-info-dialog.php';
3161
3162 new FS_Plugin_Info_Dialog( $this );
3163 }
3164 }
3165
3166 add_action( 'admin_init', array( &$this, '_admin_init_action' ) );
3167
3168 // if ( $this->is_registered() ||
3169 // $this->is_anonymous() ||
3170 // $this->is_pending_activation()
3171 // ) {
3172 // $this->_init_admin();
3173 // }
3174 }
3175
3176 /**
3177 * Should be called outside `$this->is_user_in_admin()` scope
3178 * because the updater has some logic that needs to be executed
3179 * during AJAX calls.
3180 *
3181 * Currently we need to hook to the `http_request_host_is_external` filter.
3182 * In the future, there might be additional logic added.
3183 *
3184 * @author Vova Feldman
3185 * @since 1.2.1.6
3186 */
3187 if ( $this->is_premium() && $this->has_release_on_freemius() ) {
3188 new FS_Plugin_Updater( $this );
3189 }
3190
3191 $this->do_action( 'initiated' );
3192
3193 if ( $this->_storage->prev_is_premium !== $this->_plugin->is_premium ) {
3194 if ( isset( $this->_storage->prev_is_premium ) ) {
3195 $this->apply_filters(
3196 'after_code_type_change',
3197 // New code type.
3198 $this->_plugin->is_premium
3199 );
3200 } else {
3201 // Set for code type for the first time.
3202 $this->_storage->prev_is_premium = $this->_plugin->is_premium;
3203 }
3204 }
3205
3206 if ( ! $this->is_addon() ) {
3207 if ( $this->is_registered() ) {
3208 // Fix for upgrade from versions < 1.0.9.
3209 if ( ! isset( $this->_storage->activation_timestamp ) ) {
3210 $this->_storage->activation_timestamp = WP_FS__SCRIPT_START_TIME;
3211 }
3212
3213 $this->do_action( 'after_init_plugin_registered' );
3214 } else if ( $this->is_anonymous() ) {
3215 $this->do_action( 'after_init_plugin_anonymous' );
3216 } else if ( $this->is_pending_activation() ) {
3217 $this->do_action( 'after_init_plugin_pending_activations' );
3218 }
3219 } else {
3220 if ( $this->is_registered() ) {
3221 $this->do_action( 'after_init_addon_registered' );
3222 } else if ( $this->is_anonymous() ) {
3223 $this->do_action( 'after_init_addon_anonymous' );
3224 } else if ( $this->is_pending_activation() ) {
3225 $this->do_action( 'after_init_addon_pending_activations' );
3226 }
3227 }
3228 }
3229
3230 /**
3231 * @author Leo Fajardo (@leorw)
3232 *
3233 * @since 1.2.1.5
3234 */
3235 function _stop_tracking_callback() {
3236 $this->_logger->entrance();
3237
3238 $this->check_ajax_referer( 'stop_tracking' );
3239
3240 $result = $this->stop_tracking();
3241
3242 if ( true === $result ) {
3243 self::shoot_ajax_success();
3244 }
3245
3246 $this->_logger->api_error( $result );
3247
3248 self::shoot_ajax_failure(
3249 sprintf( $this->get_text_inline( 'Unexpected API error. Please contact the %s\'s author with the following error.', 'unexpected-api-error' ), $this->_module_type ) .
3250 ( $this->is_api_error( $result ) && isset( $result->error ) ?
3251 $result->error->message :
3252 var_export( $result, true ) )
3253 );
3254 }
3255
3256 /**
3257 * @author Leo Fajardo (@leorw)
3258 * @since 1.2.1.5
3259 */
3260 function _allow_tracking_callback() {
3261 $this->_logger->entrance();
3262
3263 $this->check_ajax_referer( 'allow_tracking' );
3264
3265 $result = $this->allow_tracking();
3266
3267 if ( true === $result ) {
3268 self::shoot_ajax_success();
3269 }
3270
3271 $this->_logger->api_error( $result );
3272
3273 self::shoot_ajax_failure(
3274 sprintf( $this->get_text_inline( 'Unexpected API error. Please contact the %s\'s author with the following error.', 'unexpected-api-error' ), $this->_module_type ) .
3275 ( $this->is_api_error( $result ) && isset( $result->error ) ?
3276 $result->error->message :
3277 var_export( $result, true ) )
3278 );
3279 }
3280
3281 /**
3282 * Opt-out from usage tracking.
3283 *
3284 * Note: This will not delete the account information but will stop all tracking.
3285 *
3286 * Returns:
3287 * 1. FALSE - If the user never opted-in.
3288 * 2. TRUE - If successfully opted-out.
3289 * 3. object - API result on failure.
3290 *
3291 * @author Leo Fajardo (@leorw)
3292 * @since 1.2.1.5
3293 *
3294 * @return bool|object
3295 */
3296 function stop_tracking() {
3297 $this->_logger->entrance();
3298
3299 if ( ! $this->is_registered() ) {
3300 // User never opted-in.
3301 return false;
3302 }
3303
3304 if ( $this->is_tracking_prohibited() ) {
3305 // Already disconnected.
3306 return true;
3307 }
3308
3309 // Send update to FS.
3310 $result = $this->get_api_site_scope()->call( '/?fields=is_disconnected', 'put', array(
3311 'is_disconnected' => true
3312 ) );
3313
3314 if ( ! $this->is_api_result_entity( $result ) ||
3315 ! isset( $result->is_disconnected ) ||
3316 ! $result->is_disconnected
3317 ) {
3318 $this->_logger->api_error( $result );
3319
3320 return $result;
3321 }
3322
3323 $this->_site->is_disconnected = $result->is_disconnected;
3324 $this->_store_site();
3325
3326 $this->clear_sync_cron();
3327
3328 // Successfully disconnected.
3329 return true;
3330 }
3331
3332 /**
3333 * Opt-in back into usage tracking.
3334 *
3335 * Note: This will only work if the user opted-in previously.
3336 *
3337 * Returns:
3338 * 1. FALSE - If the user never opted-in.
3339 * 2. TRUE - If successfully opted-in back to usage tracking.
3340 * 3. object - API result on failure.
3341 *
3342 * @author Leo Fajardo (@leorw)
3343 * @since 1.2.1.5
3344 *
3345 * @return bool|object
3346 */
3347 function allow_tracking() {
3348 $this->_logger->entrance();
3349
3350 if ( ! $this->is_registered() ) {
3351 // User never opted-in.
3352 return false;
3353 }
3354
3355 if ( $this->is_tracking_allowed() ) {
3356 // Tracking already allowed.
3357 return true;
3358 }
3359
3360 $result = $this->get_api_site_scope()->call( '/?is_disconnected', 'put', array(
3361 'is_disconnected' => false
3362 ) );
3363
3364 if ( ! $this->is_api_result_entity( $result ) ||
3365 ! isset( $result->is_disconnected ) ||
3366 $result->is_disconnected
3367 ) {
3368 $this->_logger->api_error( $result );
3369
3370 return $result;
3371 }
3372
3373 $this->_site->is_disconnected = $result->is_disconnected;
3374 $this->_store_site();
3375
3376 $this->schedule_sync_cron();
3377
3378 // Successfully reconnected.
3379 return true;
3380 }
3381
3382 /**
3383 * If user opted-in and later disabled usage-tracking,
3384 * re-allow tracking for licensing and updates.
3385 *
3386 * @author Leo Fajardo (@leorw)
3387 *
3388 * @since 1.2.1.5
3389 */
3390 private function reconnect_locally() {
3391 $this->_logger->entrance();
3392
3393 if ( $this->is_tracking_prohibited() &&
3394 $this->is_registered()
3395 ) {
3396 $this->_site->is_disconnected = false;
3397 $this->_store_site();
3398 }
3399 }
3400
3401 /**
3402 * Parse plugin's settings (as defined by the plugin dev).
3403 *
3404 * @author Vova Feldman (@svovaf)
3405 * @since 1.1.7.3
3406 *
3407 * @param array $plugin_info
3408 *
3409 * @throws \Freemius_Exception
3410 */
3411 private function parse_settings( &$plugin_info ) {
3412 $this->_logger->entrance();
3413
3414 $id = $this->get_numeric_option( $plugin_info, 'id', false );
3415 $public_key = $this->get_option( $plugin_info, 'public_key', false );
3416 $secret_key = $this->get_option( $plugin_info, 'secret_key', null );
3417 $parent_id = $this->get_numeric_option( $plugin_info, 'parent_id', null );
3418 $parent_name = $this->get_option( $plugin_info, 'parent_name', null );
3419
3420 /**
3421 * @author Vova Feldman (@svovaf)
3422 * @since 1.1.9 Try to pull secret key from external config.
3423 */
3424 if ( is_null( $secret_key ) && defined( "WP_FS__{$this->_slug}_SECRET_KEY" ) ) {
3425 $secret_key = constant( "WP_FS__{$this->_slug}_SECRET_KEY" );
3426 }
3427
3428 if ( isset( $plugin_info['parent'] ) ) {
3429 $parent_id = $this->get_numeric_option( $plugin_info['parent'], 'id', null );
3430 // $parent_slug = $this->get_option( $plugin_info['parent'], 'slug', null );
3431 // $parent_public_key = $this->get_option( $plugin_info['parent'], 'public_key', null );
3432 // $parent_name = $this->get_option( $plugin_info['parent'], 'name', null );
3433 }
3434
3435 if ( false === $id ) {
3436 throw new Freemius_Exception( array(
3437 'error' => array(
3438 'type' => 'ParameterNotSet',
3439 'message' => 'Plugin id parameter is not set.',
3440 'code' => 'plugin_id_not_set',
3441 'http' => 500,
3442 )
3443 ) );
3444 }
3445 if ( false === $public_key ) {
3446 throw new Freemius_Exception( array(
3447 'error' => array(
3448 'type' => 'ParameterNotSet',
3449 'message' => 'Plugin public_key parameter is not set.',
3450 'code' => 'plugin_public_key_not_set',
3451 'http' => 500,
3452 )
3453 ) );
3454 }
3455
3456 $plugin = ( $this->_plugin instanceof FS_Plugin ) ?
3457 $this->_plugin :
3458 new FS_Plugin();
3459
3460 $plugin->update( array(
3461 'id' => $id,
3462 'type' => $this->get_option( $plugin_info, 'type', $this->_module_type),
3463 'public_key' => $public_key,
3464 'slug' => $this->_slug,
3465 'parent_plugin_id' => $parent_id,
3466 'version' => $this->get_plugin_version(),
3467 'title' => $this->get_plugin_name(),
3468 'file' => $this->_plugin_basename,
3469 'is_premium' => $this->get_bool_option( $plugin_info, 'is_premium', true ),
3470 'is_live' => $this->get_bool_option( $plugin_info, 'is_live', true ),
3471 'affiliate_moderation' => $this->get_option( $plugin_info, 'has_affiliation' ),
3472 ) );
3473
3474 if ( $plugin->is_updated() ) {
3475 // Update plugin details.
3476 $this->_plugin = FS_Plugin_Manager::instance( $this->_module_id )->store( $plugin );
3477 }
3478 // Set the secret key after storing the plugin, we don't want to store the key in the storage.
3479 $this->_plugin->secret_key = $secret_key;
3480
3481 if ( ! isset( $plugin_info['menu'] ) ) {
3482 $plugin_info['menu'] = array();
3483
3484 if ( ! empty( $this->_storage->sdk_last_version ) &&
3485 version_compare( $this->_storage->sdk_last_version, '1.1.2', '<=' )
3486 ) {
3487 // Backward compatibility to 1.1.2
3488 $plugin_info['menu']['slug'] = isset( $plugin_info['menu_slug'] ) ?
3489 $plugin_info['menu_slug'] :
3490 $this->_slug;
3491 }
3492 }
3493
3494 $this->_menu = FS_Admin_Menu_Manager::instance(
3495 $this->_module_id,
3496 $this->_module_type,
3497 $this->get_unique_affix()
3498 );
3499
3500 $this->_menu->init( $plugin_info['menu'], $this->is_addon() );
3501
3502 $this->_has_addons = $this->get_bool_option( $plugin_info, 'has_addons', false );
3503 $this->_has_paid_plans = $this->get_bool_option( $plugin_info, 'has_paid_plans', true );
3504 $this->_has_premium_version = $this->get_bool_option( $plugin_info, 'has_premium_version', $this->_has_paid_plans );
3505 $this->_ignore_pending_mode = $this->get_bool_option( $plugin_info, 'ignore_pending_mode', false );
3506 $this->_is_org_compliant = $this->get_bool_option( $plugin_info, 'is_org_compliant', true );
3507 $this->_is_premium_only = $this->get_bool_option( $plugin_info, 'is_premium_only', false );
3508 if ( $this->_is_premium_only ) {
3509 // If premium only plugin, disable anonymous mode.
3510 $this->_enable_anonymous = false;
3511 $this->_anonymous_mode = false;
3512 } else {
3513 $this->_enable_anonymous = $this->get_bool_option( $plugin_info, 'enable_anonymous', true );
3514 $this->_anonymous_mode = $this->get_bool_option( $plugin_info, 'anonymous_mode', false );
3515 }
3516 $this->_permissions = $this->get_option( $plugin_info, 'permissions', array() );
3517
3518 if ( ! empty( $plugin_info['trial'] ) ) {
3519 $this->_trial_days = $this->get_numeric_option(
3520 $plugin_info['trial'],
3521 'days',
3522 // Default to 0 - trial without days specification.
3523 0
3524 );
3525
3526 $this->_is_trial_require_payment = $this->get_bool_option( $plugin_info['trial'], 'is_require_payment', false );
3527 }
3528 }
3529
3530 /**
3531 * @param string[] $options
3532 * @param string $key
3533 * @param mixed $default
3534 *
3535 * @return bool
3536 */
3537 private function get_option( &$options, $key, $default = false ) {
3538 return ! empty( $options[ $key ] ) ? $options[ $key ] : $default;
3539 }
3540
3541 private function get_bool_option( &$options, $key, $default = false ) {
3542 return isset( $options[ $key ] ) && is_bool( $options[ $key ] ) ? $options[ $key ] : $default;
3543 }
3544
3545 private function get_numeric_option( &$options, $key, $default = false ) {
3546 return isset( $options[ $key ] ) && is_numeric( $options[ $key ] ) ? $options[ $key ] : $default;
3547 }
3548
3549 /**
3550 * Gate keeper.
3551 *
3552 * @author Vova Feldman (@svovaf)
3553 * @since 1.1.7.3
3554 *
3555 * @return bool
3556 */
3557 private function should_stop_execution() {
3558 if ( empty( $this->_storage->was_plugin_loaded ) ) {
3559 /**
3560 * Don't execute Freemius until plugin was fully loaded at least once,
3561 * to give the opportunity for the activation hook to run before pinging
3562 * the API for connectivity test. This logic is relevant for the
3563 * identification of new plugin install vs. plugin update.
3564 *
3565 * @author Vova Feldman (@svovaf)
3566 * @since 1.1.9
3567 */
3568 return true;
3569 }
3570
3571 if ( $this->is_activation_mode() ) {
3572 if ( ! is_admin() ) {
3573 /**
3574 * If in activation mode, don't execute Freemius outside of the
3575 * admin dashboard.
3576 *
3577 * @author Vova Feldman (@svovaf)
3578 * @since 1.1.7.3
3579 */
3580 return true;
3581 }
3582
3583 if ( ! WP_FS__IS_HTTP_REQUEST ) {
3584 /**
3585 * If in activation and executed without HTTP context (e.g. CLI, Cronjob),
3586 * then don't start Freemius.
3587 *
3588 * @author Vova Feldman (@svovaf)
3589 * @since 1.1.6.3
3590 *
3591 * @link https://wordpress.org/support/topic/errors-in-the-freemius-class-when-running-in-wordpress-in-cli
3592 */
3593 return true;
3594 }
3595
3596 if ( self::is_cron() ) {
3597 /**
3598 * If in activation mode, don't execute Freemius during wp crons
3599 * (wp crons have HTTP context - called as HTTP request).
3600 *
3601 * @author Vova Feldman (@svovaf)
3602 * @since 1.1.7.3
3603 */
3604 return true;
3605 }
3606
3607 if ( self::is_ajax() &&
3608 ! $this->_admin_notices->has_sticky( 'failed_connect_api_first' ) &&
3609 ! $this->_admin_notices->has_sticky( 'failed_connect_api' )
3610 ) {
3611 /**
3612 * During activation, if running in AJAX mode, unless there's a sticky
3613 * connectivity issue notice, don't run Freemius.
3614 *
3615 * @author Vova Feldman (@svovaf)
3616 * @since 1.1.7.3
3617 */
3618 return true;
3619 }
3620 }
3621
3622 return false;
3623 }
3624
3625 /**
3626 * Triggered after code type has changed.
3627 *
3628 * @author Vova Feldman (@svovaf)
3629 * @since 1.1.9.1
3630 */
3631 function _after_code_type_change() {
3632 $this->_logger->entrance();
3633
3634 if ( $this->is_theme() ) {
3635 // Expire the cache of the previous tabs since the theme may
3636 // have setting updates after code type has changed.
3637 $this->_cache->expire( 'tabs' );
3638 $this->_cache->expire( 'tabs_stylesheets' );
3639 }
3640
3641 if ( $this->is_registered() ) {
3642 if ( ! $this->is_addon() ) {
3643 add_action(
3644 is_admin() ? 'admin_init' : 'init',
3645 array( &$this, '_plugin_code_type_changed' )
3646 );
3647 }
3648
3649 if ( $this->is_premium() ) {
3650 // Purge cached payments after switching to the premium version.
3651 // @todo This logic doesn't handle purging the cache for serviceware module upgrade.
3652 $this->get_api_user_scope()->purge_cache( "/plugins/{$this->_module_id}/payments.json?include_addons=true" );
3653 }
3654 }
3655 }
3656
3657 /**
3658 * Handles plugin's code type change (free <--> premium).
3659 *
3660 * @author Vova Feldman (@svovaf)
3661 * @since 1.0.9
3662 */
3663 function _plugin_code_type_changed() {
3664 $this->_logger->entrance();
3665
3666 if ( $this->is_premium() ) {
3667 $this->reconnect_locally();
3668
3669 // Activated premium code.
3670 $this->do_action( 'after_premium_version_activation' );
3671
3672 // Remove all sticky messages related to download of the premium version.
3673 $this->_admin_notices->remove_sticky( array(
3674 'trial_started',
3675 'plan_upgraded',
3676 'plan_changed',
3677 'license_activated',
3678 ) );
3679
3680 if ( ! $this->is_only_premium() ) {
3681 $this->_admin_notices->add_sticky(
3682 sprintf( $this->get_text_inline( 'Premium %s version was successfully activated.', 'premium-activated-message' ), $this->_module_type ),
3683 'premium_activated',
3684 $this->get_text_x_inline( 'W00t',
3685 'Used to express elation, enthusiasm, or triumph (especially in electronic communication).', 'woot' ) . '!'
3686 );
3687 }
3688 } else {
3689 // Remove sticky message related to premium code activation.
3690 $this->_admin_notices->remove_sticky( 'premium_activated' );
3691
3692 // Activated free code (after had the premium before).
3693 $this->do_action( 'after_free_version_reactivation' );
3694
3695 if ( $this->is_paying() && ! $this->is_premium() ) {
3696 $this->_admin_notices->add_sticky(
3697 sprintf(
3698 /* translators: %s: License type (e.g. you have a professional license) */
3699 $this->get_text_inline( 'You have a %s license.', 'you-have-x-license' ),
3700 $this->_site->plan->title
3701 ) . $this->get_complete_upgrade_instructions(),
3702 'plan_upgraded',
3703 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
3704 );
3705 }
3706 }
3707
3708 // Schedule code type changes event.
3709 $this->schedule_install_sync();
3710
3711 /**
3712 * Unregister the uninstall hook for the other version of the plugin (with different code type) to avoid
3713 * triggering a fatal error when uninstalling that plugin. For example, after deactivating the "free" version
3714 * of a specific plugin, its uninstall hook should be unregistered after the "premium" version has been
3715 * activated. If we don't do that, a fatal error will occur when we try to uninstall the "free" version since
3716 * the main file of the "free" version will be loaded first before calling the hooked callback. Since the
3717 * free and premium versions are almost identical (same class or have same functions), a fatal error like
3718 * "Cannot redeclare class MyClass" or "Cannot redeclare my_function()" will occur.
3719 */
3720 $this->unregister_uninstall_hook();
3721
3722 $this->clear_module_main_file_cache();
3723
3724 // Update is_premium of latest version.
3725 $this->_storage->prev_is_premium = $this->_plugin->is_premium;
3726 }
3727
3728 #endregion
3729
3730 #----------------------------------------------------------------------------------
3731 #region Add-ons
3732 #----------------------------------------------------------------------------------
3733
3734 /**
3735 * Check if add-on installed and activated on site.
3736 *
3737 * @author Vova Feldman (@svovaf)
3738 * @since 1.0.6
3739 *
3740 * @param string|number $id_or_slug
3741 * @param bool|null $is_premium Since 1.2.1.7 can check for specified add-on version.
3742 *
3743 * @return bool
3744 */
3745 function is_addon_activated( $id_or_slug, $is_premium = null ) {
3746 $this->_logger->entrance();
3747
3748 $addon_id = self::get_module_id( $id_or_slug );
3749 $is_activated = self::has_instance( $addon_id );
3750
3751 if ( ! $is_activated ) {
3752 return false;
3753 }
3754
3755 if ( is_bool( $is_premium ) ) {
3756 // Check if the specified code version is activate.
3757 $addon = $this->get_addon_instance( $addon_id );
3758 $is_activated = ( $is_premium === $addon->is_premium() );
3759 }
3760
3761 return $is_activated;
3762 }
3763
3764 /**
3765 * Check if add-on was connected to install
3766 *
3767 * @author Vova Feldman (@svovaf)
3768 * @since 1.1.7
3769 *
3770 * @param string|number $id_or_slug
3771 *
3772 * @return bool
3773 */
3774 function is_addon_connected( $id_or_slug ) {
3775 $this->_logger->entrance();
3776
3777 $sites = self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN );
3778
3779 $addon_id = self::get_module_id( $id_or_slug );
3780 $addon = $this->get_addon( $addon_id );
3781 $slug = $addon->slug;
3782 if ( ! isset( $sites[ $slug ] ) ) {
3783 return false;
3784 }
3785
3786 $site = $sites[ $slug ];
3787
3788 $plugin = FS_Plugin_Manager::instance( $addon_id )->get();
3789
3790 if ( $plugin->parent_plugin_id != $this->_plugin->id ) {
3791 // The given slug do NOT belong to any of the plugin's add-ons.
3792 return false;
3793 }
3794
3795 return ( is_object( $site ) &&
3796 is_numeric( $site->id ) &&
3797 is_numeric( $site->user_id ) &&
3798 is_object( $site->plan )
3799 );
3800 }
3801
3802 /**
3803 * Determines if add-on installed.
3804 *
3805 * NOTE: This is a heuristic and only works if the folder/file named as the slug.
3806 *
3807 * @author Vova Feldman (@svovaf)
3808 * @since 1.0.6
3809 *
3810 * @param string|number $id_or_slug
3811 *
3812 * @return bool
3813 */
3814 function is_addon_installed( $id_or_slug ) {
3815 $this->_logger->entrance();
3816
3817 $addon_id = self::get_module_id( $id_or_slug );
3818
3819 return file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $this->get_addon_basename( $addon_id ) ) );
3820 }
3821
3822 /**
3823 * Get add-on basename.
3824 *
3825 * @author Vova Feldman (@svovaf)
3826 * @since 1.0.6
3827 *
3828 * @param string|number $id_or_slug
3829 *
3830 * @return string
3831 */
3832 function get_addon_basename( $id_or_slug ) {
3833 $addon_id = self::get_module_id( $id_or_slug );
3834
3835 if ( $this->is_addon_activated( $addon_id ) ) {
3836 return self::instance( $addon_id )->get_plugin_basename();
3837 }
3838
3839 $addon = $this->get_addon( $addon_id );
3840 $premium_basename = "{$addon->slug}-premium/{$addon->slug}.php";
3841
3842 if ( file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $premium_basename ) ) ) {
3843 return $premium_basename;
3844 }
3845
3846 $all_plugins = $this->get_all_plugins();
3847
3848 foreach ( $all_plugins as $basename => &$data ) {
3849 if ( $addon->slug === $data['slug'] ||
3850 $addon->slug . '-premium' === $data['slug']
3851 ) {
3852 return $basename;
3853 }
3854 }
3855
3856 $free_basename = "{$addon->slug}/{$addon->slug}.php";
3857
3858 return $free_basename;
3859 }
3860
3861 /**
3862 * Get installed add-ons instances.
3863 *
3864 * @author Vova Feldman (@svovaf)
3865 * @since 1.0.6
3866 *
3867 * @return Freemius[]
3868 */
3869 function get_installed_addons() {
3870 $installed_addons = array();
3871 foreach ( self::$_instances as $instance ) {
3872 if ( $instance->is_addon() && is_object( $instance->_parent_plugin ) ) {
3873 if ( $this->_plugin->id == $instance->_parent_plugin->id ) {
3874 $installed_addons[] = $instance;
3875 }
3876 }
3877 }
3878
3879 return $installed_addons;
3880 }
3881
3882 /**
3883 * Check if any add-ons of the plugin are installed.
3884 *
3885 * @author Leo Fajardo (@leorw)
3886 * @since 1.1.1
3887 *
3888 * @return bool
3889 */
3890 function has_installed_addons() {
3891 if ( ! $this->has_addons() ) {
3892 return false;
3893 }
3894
3895 foreach ( self::$_instances as $instance ) {
3896 if ( $instance->is_addon() && is_object( $instance->_parent_plugin ) ) {
3897 if ( $this->_plugin->id == $instance->_parent_plugin->id ) {
3898 return true;
3899 }
3900 }
3901 }
3902
3903 return false;
3904 }
3905
3906 /**
3907 * Tell Freemius that the current plugin is an add-on.
3908 *
3909 * @author Vova Feldman (@svovaf)
3910 * @since 1.0.6
3911 *
3912 * @param number $parent_plugin_id The parent plugin ID
3913 */
3914 function init_addon( $parent_plugin_id ) {
3915 $this->_plugin->parent_plugin_id = $parent_plugin_id;
3916 }
3917
3918 /**
3919 * @author Vova Feldman (@svovaf)
3920 * @since 1.0.6
3921 *
3922 * @return bool
3923 */
3924 function is_addon() {
3925 return isset( $this->_plugin->parent_plugin_id ) && is_numeric( $this->_plugin->parent_plugin_id );
3926 }
3927
3928 /**
3929 * Deactivate add-on if it's premium only and the user does't have a valid license.
3930 *
3931 * @param bool $is_after_trial_cancel
3932 *
3933 * @return bool If add-on was deactivated.
3934 */
3935 private function deactivate_premium_only_addon_without_license( $is_after_trial_cancel = false ) {
3936 if ( ! $this->has_free_plan() &&
3937 ! $this->has_features_enabled_license() &&
3938 ! $this->_has_premium_license()
3939 ) {
3940 if ( $this->is_registered() ) {
3941 // IF wrapper is turned off because activation_timestamp is currently only stored for plugins (not addons).
3942 // if (empty($this->_storage->activation_timestamp) ||
3943 // (WP_FS__SCRIPT_START_TIME - $this->_storage->activation_timestamp) > 30
3944 // ) {
3945 /**
3946 * @todo When it's first fail, there's no reason to try and re-sync because the licenses were just synced after initial activation.
3947 *
3948 * Retry syncing the user add-on licenses.
3949 */
3950 // Sync licenses.
3951 $this->_sync_licenses();
3952 // }
3953
3954 // Try to activate premium license.
3955 $this->_activate_license( true );
3956 }
3957
3958 if ( ! $this->has_free_plan() &&
3959 ! $this->has_features_enabled_license() &&
3960 ! $this->_has_premium_license()
3961 ) {
3962 // @todo Check if deactivate plugins also call the deactivation hook.
3963
3964 $this->_parent->_admin_notices->add_sticky(
3965 sprintf(
3966 ($is_after_trial_cancel ?
3967 $this->_parent->get_text_inline(
3968 '%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.',
3969 'addon-trial-cancelled-message'
3970 ) :
3971 $this->_parent->get_text_inline(
3972 '%s is a premium only add-on. You have to purchase a license first before activating the plugin.',
3973 'addon-no-license-message'
3974 )
3975 ),
3976 '<b>' . $this->_plugin->title . '</b>'
3977 ) . ' ' . sprintf(
3978 '<a href="%s" aria-label="%s" class="button button-primary" style="margin-left: 10px; vertical-align: middle;">%s &nbsp;&#10140;</a>',
3979 $this->_parent->addon_url( $this->_slug ),
3980 esc_attr( sprintf( $this->_parent->get_text_inline( 'More information about %s', 'more-information-about-x' ), $this->_plugin->title ) ),
3981 $this->_parent->get_text_inline( 'Purchase License', 'purchase-license' )
3982 ),
3983 'no_addon_license_' . $this->_slug,
3984 ( $is_after_trial_cancel ? '' : $this->_parent->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...' ),
3985 ( $is_after_trial_cancel ? 'success' : 'error' )
3986 );
3987
3988 deactivate_plugins( array( $this->_plugin_basename ), true );
3989
3990 return true;
3991 }
3992 }
3993
3994 return false;
3995 }
3996
3997 #endregion
3998
3999 #----------------------------------------------------------------------------------
4000 #region Sandbox
4001 #----------------------------------------------------------------------------------
4002
4003 /**
4004 * Set Freemius into sandbox mode for debugging.
4005 *
4006 * @author Vova Feldman (@svovaf)
4007 * @since 1.0.4
4008 *
4009 * @param string $secret_key
4010 */
4011 function init_sandbox( $secret_key ) {
4012 $this->_plugin->secret_key = $secret_key;
4013
4014 // Update plugin details.
4015 FS_Plugin_Manager::instance( $this->_module_id )->update( $this->_plugin, true );
4016 }
4017
4018 /**
4019 * Check if running payments in sandbox mode.
4020 *
4021 * @author Vova Feldman (@svovaf)
4022 * @since 1.0.4
4023 *
4024 * @return bool
4025 */
4026 function is_payments_sandbox() {
4027 return ( ! $this->is_live() ) || isset( $this->_plugin->secret_key );
4028 }
4029
4030 #endregion
4031
4032 /**
4033 * Check if running test vs. live plugin.
4034 *
4035 * @author Vova Feldman (@svovaf)
4036 * @since 1.0.5
4037 *
4038 * @return bool
4039 */
4040 function is_live() {
4041 return $this->_plugin->is_live;
4042 }
4043
4044 /**
4045 * Check if the user skipped connecting the account with Freemius.
4046 *
4047 * @author Vova Feldman (@svovaf)
4048 * @since 1.0.7
4049 *
4050 * @return bool
4051 */
4052 function is_anonymous() {
4053 if ( ! isset( $this->_is_anonymous ) ) {
4054 if ( ! isset( $this->_storage->is_anonymous ) ) {
4055 // Not skipped.
4056 $this->_is_anonymous = false;
4057 } else if ( is_bool( $this->_storage->is_anonymous ) ) {
4058 // For back compatibility, since the variable was boolean before.
4059 $this->_is_anonymous = $this->_storage->is_anonymous;
4060
4061 // Upgrade stored data format to 1.1.3 format.
4062 $this->set_anonymous_mode( $this->_storage->is_anonymous );
4063 } else {
4064 // Version 1.1.3 and later.
4065 $this->_is_anonymous = $this->_storage->is_anonymous['is'];
4066 }
4067 }
4068
4069 return $this->_is_anonymous;
4070 }
4071
4072 /**
4073 * Check if user connected his account and install pending email activation.
4074 *
4075 * @author Vova Feldman (@svovaf)
4076 * @since 1.0.7
4077 *
4078 * @return bool
4079 */
4080 function is_pending_activation() {
4081 return $this->_storage->get( 'is_pending_activation', false );
4082 }
4083
4084 /**
4085 * Check if plugin must be WordPress.org compliant.
4086 *
4087 * @since 1.0.7
4088 *
4089 * @return bool
4090 */
4091 function is_org_repo_compliant() {
4092 return $this->_is_org_compliant;
4093 }
4094
4095 #----------------------------------------------------------------------------------
4096 #region Daily Sync Cron
4097 #----------------------------------------------------------------------------------
4098
4099 /**
4100 * @author Vova Feldman (@svovaf)
4101 * @since 1.1.7.3
4102 */
4103 private function run_manual_sync() {
4104 self::require_pluggable_essentials();
4105
4106 if ( ! $this->is_user_admin() ) {
4107 return;
4108 }
4109
4110 // Run manual sync.
4111 $this->_sync_cron();
4112
4113 // Reschedule next cron to run 24 hours from now (performance optimization).
4114 $this->clear_sync_cron();
4115
4116 $this->schedule_sync_cron( time() + WP_FS__TIME_24_HOURS_IN_SEC, false );
4117 }
4118
4119 /**
4120 * Data sync cron job. Replaces the background sync non blocking HTTP request
4121 * that doesn't halt page loading.
4122 *
4123 * @author Vova Feldman (@svovaf)
4124 * @since 1.1.7.3
4125 */
4126 function _sync_cron() {
4127 $this->_logger->entrance();
4128
4129 // Store the last time data sync was executed.
4130 $this->_storage->sync_timestamp = time();
4131
4132 // Check if API is temporary down.
4133 if ( FS_Api::is_temporary_down() ) {
4134 return;
4135 }
4136
4137 // @todo Add logic that identifies API latency, and reschedule the next background sync randomly between 8-16 hours.
4138
4139 if ( $this->is_registered() ) {
4140 if ( $this->has_paid_plan() ) {
4141 // Initiate background plan sync.
4142 $this->_sync_license( true );
4143
4144 if ( $this->is_paying() ) {
4145 // Check for premium plugin updates.
4146 $this->check_updates( true );
4147 }
4148 } else {
4149 // Sync install (only if something changed locally).
4150 $this->sync_install();
4151 }
4152 }
4153
4154 $this->do_action( 'after_sync_cron' );
4155 }
4156
4157 /**
4158 * Check if sync was executed in the last $period of seconds.
4159 *
4160 * @author Vova Feldman (@svovaf)
4161 * @since 1.1.7.3
4162 *
4163 * @param int $period In seconds
4164 *
4165 * @return bool
4166 */
4167 private function is_sync_executed( $period = WP_FS__TIME_24_HOURS_IN_SEC ) {
4168 if ( ! isset( $this->_storage->sync_timestamp ) ) {
4169 return false;
4170 }
4171
4172 return ( $this->_storage->sync_timestamp > ( WP_FS__SCRIPT_START_TIME - $period ) );
4173 }
4174
4175 /**
4176 * @author Vova Feldman (@svovaf)
4177 * @since 1.1.7.3
4178 *
4179 * @return bool
4180 */
4181 private function is_sync_cron_on() {
4182 /**
4183 * @var object $sync_cron_data
4184 */
4185 $sync_cron_data = $this->_storage->get( 'sync_cron', null );
4186
4187 return ( ! is_null( $sync_cron_data ) && true === $sync_cron_data->on );
4188 }
4189
4190 /**
4191 * @author Vova Feldman (@svovaf)
4192 * @since 1.1.7.3
4193 *
4194 * @param int $start_at Defaults to now.
4195 * @param bool $randomize_start If true, schedule first job randomly during the next 12 hours. Otherwise,
4196 * schedule job to start right away.
4197 */
4198 private function schedule_sync_cron( $start_at = WP_FS__SCRIPT_START_TIME, $randomize_start = true ) {
4199 $this->_logger->entrance();
4200
4201 if ( $randomize_start ) {
4202 // Schedule first sync with a random 12 hour time range from now.
4203 $start_at += rand( 0, ( WP_FS__TIME_24_HOURS_IN_SEC / 2 ) );
4204 }
4205
4206 // Schedule daily WP cron.
4207 wp_schedule_event(
4208 $start_at,
4209 'daily',
4210 $this->get_action_tag( 'data_sync' )
4211 );
4212
4213 $this->_storage->store( 'sync_cron', (object) array(
4214 'version' => $this->get_plugin_version(),
4215 'sdk_version' => $this->version,
4216 'timestamp' => WP_FS__SCRIPT_START_TIME,
4217 'on' => true,
4218 ) );
4219 }
4220
4221 /**
4222 * Add the actual sync function to the cron job hook.
4223 *
4224 * @author Vova Feldman (@svovaf)
4225 * @since 1.1.7.3
4226 */
4227 private function hook_callback_to_sync_cron() {
4228 $this->add_action( 'data_sync', array( &$this, '_sync_cron' ) );
4229 }
4230
4231 /**
4232 * @author Vova Feldman (@svovaf)
4233 * @since 1.1.7.3
4234 */
4235 private function clear_sync_cron() {
4236 $this->_logger->entrance();
4237
4238 if ( ! $this->is_sync_cron_on() ) {
4239 return;
4240 }
4241
4242 $this->_storage->remove( 'sync_cron' );
4243
4244 wp_clear_scheduled_hook( $this->get_action_tag( 'data_sync' ) );
4245 }
4246
4247 /**
4248 * Unix timestamp for next sync cron execution or false if not scheduled.
4249 *
4250 * @author Vova Feldman (@svovaf)
4251 * @since 1.1.7.3
4252 *
4253 * @return int|false
4254 */
4255 function next_sync_cron() {
4256 $this->_logger->entrance();
4257
4258 if ( ! $this->is_sync_cron_on() ) {
4259 return false;
4260 }
4261
4262 return wp_next_scheduled( $this->get_action_tag( 'data_sync' ) );
4263 }
4264
4265 /**
4266 * Unix timestamp for previous sync cron execution or false if never executed.
4267 *
4268 * @author Vova Feldman (@svovaf)
4269 * @since 1.1.7.3
4270 *
4271 * @return int|false
4272 */
4273 function last_sync_cron() {
4274 $this->_logger->entrance();
4275
4276 return $this->_storage->get( 'sync_timestamp' );
4277 }
4278
4279 #endregion Daily Sync Cron ------------------------------------------------------------------
4280
4281 #----------------------------------------------------------------------------------
4282 #region Async Install Sync
4283 #----------------------------------------------------------------------------------
4284
4285 /**
4286 * @author Vova Feldman (@svovaf)
4287 * @since 1.1.7.3
4288 *
4289 * @return bool
4290 */
4291 private function is_install_sync_scheduled() {
4292 /**
4293 * @var object $cron_data
4294 */
4295 $cron_data = $this->_storage->get( 'install_sync_cron', null );
4296
4297 return ( ! is_null( $cron_data ) && true === $cron_data->on );
4298 }
4299
4300 /**
4301 * Instead of running blocking install sync event, execute non blocking scheduled wp-cron.
4302 *
4303 * @author Vova Feldman (@svovaf)
4304 * @since 1.1.7.3
4305 */
4306 private function schedule_install_sync() {
4307 $this->_logger->entrance();
4308
4309 $this->clear_install_sync_cron();
4310
4311 // Schedule immediate install sync.
4312 wp_schedule_single_event(
4313 WP_FS__SCRIPT_START_TIME,
4314 $this->get_action_tag( 'install_sync' )
4315 );
4316
4317 $this->_storage->store( 'install_sync_cron', (object) array(
4318 'version' => $this->get_plugin_version(),
4319 'sdk_version' => $this->version,
4320 'timestamp' => WP_FS__SCRIPT_START_TIME,
4321 'on' => true,
4322 ) );
4323 }
4324
4325 /**
4326 * Unix timestamp for previous install sync cron execution or false if never executed.
4327 *
4328 * @todo There's some very strange bug that $this->_storage->install_sync_timestamp value is not being
4329 * updated. But for sure the sync event is working.
4330 *
4331 * @author Vova Feldman (@svovaf)
4332 * @since 1.1.7.3
4333 *
4334 * @return int|false
4335 */
4336 function last_install_sync() {
4337 $this->_logger->entrance();
4338
4339 return $this->_storage->get( 'install_sync_timestamp' );
4340 }
4341
4342 /**
4343 * Unix timestamp for next install sync cron execution or false if not scheduled.
4344 *
4345 * @author Vova Feldman (@svovaf)
4346 * @since 1.1.7.3
4347 *
4348 * @return int|false
4349 */
4350 function next_install_sync() {
4351 $this->_logger->entrance();
4352
4353 if ( ! $this->is_install_sync_scheduled() ) {
4354 return false;
4355 }
4356
4357 return wp_next_scheduled( $this->get_action_tag( 'install_sync' ) );
4358 }
4359
4360 /**
4361 * Add the actual install sync function to the cron job hook.
4362 *
4363 * @author Vova Feldman (@svovaf)
4364 * @since 1.1.7.3
4365 */
4366 private function hook_callback_to_install_sync() {
4367 $this->add_action( 'install_sync', array( &$this, '_run_sync_install' ) );
4368 }
4369
4370 /**
4371 * @author Vova Feldman (@svovaf)
4372 * @since 1.1.7.3
4373 */
4374 private function clear_install_sync_cron() {
4375 $this->_logger->entrance();
4376
4377 if ( ! $this->is_install_sync_scheduled() ) {
4378 return;
4379 }
4380
4381 $this->_storage->remove( 'install_sync_cron' );
4382
4383 wp_clear_scheduled_hook( $this->get_action_tag( 'install_sync' ) );
4384 }
4385
4386 /**
4387 * @author Vova Feldman (@svovaf)
4388 * @since 1.1.7.3
4389 */
4390 public function _run_sync_install() {
4391 $this->_logger->entrance();
4392
4393 // Update last install sync timestamp.
4394 $this->_storage->install_sync_timestamp = time();
4395
4396 $this->sync_install( array(), true );
4397 }
4398
4399 #endregion Async Install Sync ------------------------------------------------------------------
4400
4401 /**
4402 * Show a notice that activation is currently pending.
4403 *
4404 * @author Vova Feldman (@svovaf)
4405 * @since 1.0.7
4406 *
4407 * @param bool|string $email
4408 * @param bool $is_pending_trial Since 1.2.1.5
4409 */
4410 function _add_pending_activation_notice( $email = false, $is_pending_trial = false ) {
4411 if ( ! is_string( $email ) ) {
4412 $current_user = self::_get_current_wp_user();
4413 $email = $current_user->user_email;
4414 }
4415
4416 $this->_admin_notices->add_sticky(
4417 sprintf(
4418 $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' ),
4419 '<b>' . $this->get_plugin_name() . '</b>',
4420 '<b>' . $email . '</b>',
4421 ( $is_pending_trial ?
4422 $this->get_text_inline( 'start the trial', 'start-the-trial' ) :
4423 $this->get_text_inline( 'complete the install', 'complete-the-install' ) )
4424 ),
4425 'activation_pending',
4426 'Thanks!'
4427 );
4428 }
4429
4430 /**
4431 * Check if currently in plugin activation.
4432 *
4433 * @author Vova Feldman (@svovaf)
4434 * @since 1.1.4
4435 *
4436 * @return bool
4437 */
4438 function is_plugin_activation() {
4439 return get_option( 'fs_'
4440 . ( $this->is_plugin() ? '' : $this->_module_type . '_' )
4441 . "{$this->_slug}_activated", false );
4442 }
4443
4444 /**
4445 *
4446 * NOTE: admin_menu action executed before admin_init.
4447 *
4448 * @author Vova Feldman (@svovaf)
4449 * @since 1.0.7
4450 */
4451 function _admin_init_action() {
4452 /**
4453 * Automatically redirect to connect/activation page after plugin activation.
4454 *
4455 * @since 1.1.7 Do NOT redirect to opt-in when running in network admin mode.
4456 */
4457 if ( $this->is_plugin_activation() ) {
4458 delete_option( 'fs_'
4459 . ( $this->is_plugin() ? '' : $this->_module_type . '_' )
4460 . "{$this->_slug}_activated" );
4461
4462 if ( ! function_exists( 'is_network_admin' ) || ! is_network_admin() ) {
4463 $this->_redirect_on_activation_hook();
4464
4465 return;
4466 }
4467 }
4468
4469 if ( fs_request_is_action( $this->get_unique_affix() . '_skip_activation' ) ) {
4470 check_admin_referer( $this->get_unique_affix() . '_skip_activation' );
4471
4472 $this->skip_connection();
4473
4474 fs_redirect( $this->get_after_activation_url( 'after_skip_url' ) );
4475 }
4476
4477 if ( ! $this->is_addon() && ! $this->is_registered() && ! $this->is_anonymous() ) {
4478 if ( ! $this->is_pending_activation() ) {
4479 if ( ! $this->_menu->is_main_settings_page() ) {
4480 /**
4481 * If a user visits any other admin page before activating the premium-only theme with a valid
4482 * license, reactivate the previous theme.
4483 *
4484 * @author Leo Fajardo (@leorw)
4485 * @since 1.2.2
4486 */
4487 if ( $this->is_theme()
4488 && $this->is_only_premium()
4489 && ! $this->has_settings_menu()
4490 && ! isset( $_REQUEST['fs_action'] )
4491 && $this->can_activate_previous_theme()
4492 ) {
4493 $this->activate_previous_theme();
4494
4495 return;
4496 }
4497
4498 if ( $this->is_plugin_new_install() || $this->is_only_premium() ) {
4499 // Show notice for new plugin installations.
4500 $this->_admin_notices->add(
4501 sprintf(
4502 $this->get_text_inline( 'You are just one step away - %s', 'you-are-step-away' ),
4503 sprintf( '<b><a href="%s">%s</a></b>',
4504 $this->get_activation_url(),
4505 sprintf( $this->get_text_x_inline( 'Complete "%s" Activation Now',
4506 '%s - plugin name. As complete "PluginX" activation now', 'activate-x-now' ), $this->get_plugin_name() )
4507 )
4508 ),
4509 '',
4510 'update-nag'
4511 );
4512 } else {
4513 if ( ! isset( $this->_storage->sticky_optin_added ) ) {
4514 $this->_storage->sticky_optin_added = true;
4515
4516 // Show notice for new plugin installations.
4517 $this->_admin_notices->add_sticky(
4518 sprintf(
4519 $this->get_text_inline( 'We made a few tweaks to the %s, %s', 'few-plugin-tweaks' ),
4520 $this->_module_type,
4521 sprintf( '<b><a href="%s">%s</a></b>',
4522 $this->get_activation_url(),
4523 sprintf( $this->get_text_inline( 'Opt in to make "%s" Better!', 'optin-x-now' ), $this->get_plugin_name() )
4524 )
4525 ),
4526 'connect_account',
4527 '',
4528 'update-nag'
4529 );
4530 }
4531
4532 if ( $this->has_filter( 'optin_pointer_element' ) ) {
4533 // Don't show admin nag if plugin update.
4534 wp_enqueue_script( 'wp-pointer' );
4535 wp_enqueue_style( 'wp-pointer' );
4536
4537 $this->_enqueue_connect_essentials();
4538
4539 add_action( 'admin_print_footer_scripts', array(
4540 $this,
4541 '_add_connect_pointer_script'
4542 ) );
4543 }
4544 }
4545 }
4546 }
4547
4548 if ( $this->is_theme() &&
4549 $this->_menu->is_main_settings_page()
4550 ) {
4551 $this->_show_theme_activation_optin_dialog();
4552 }
4553 }
4554
4555 $this->_add_upgrade_action_link();
4556 }
4557
4558 /**
4559 * Enqueue connect requires scripts and styles.
4560 *
4561 * @author Vova Feldman (@svovaf)
4562 * @since 1.1.4
4563 */
4564 function _enqueue_connect_essentials() {
4565 wp_enqueue_script( 'jquery' );
4566 wp_enqueue_script( 'json2' );
4567
4568 fs_enqueue_local_script( 'postmessage', 'nojquery.ba-postmessage.min.js' );
4569 fs_enqueue_local_script( 'fs-postmessage', 'postmessage.js' );
4570
4571 fs_enqueue_local_style( 'fs_connect', '/admin/connect.css' );
4572 }
4573
4574 /**
4575 * Add connect / opt-in pointer.
4576 *
4577 * @author Vova Feldman (@svovaf)
4578 * @since 1.1.4
4579 */
4580 function _add_connect_pointer_script() {
4581 $vars = array( 'id' => $this->_module_id );
4582 $pointer_content = fs_get_template( 'connect.php', $vars );
4583 ?>
4584 <script type="text/javascript">// <![CDATA[
4585 jQuery(document).ready(function ($) {
4586 if ('undefined' !== typeof(jQuery().pointer)) {
4587
4588 var element = <?php echo $this->apply_filters( 'optin_pointer_element', '$("#non_existing_element");' ) ?>;
4589
4590 if (element.length > 0) {
4591 var optin = $(element).pointer($.extend(true, {}, {
4592 content : <?php echo json_encode( $pointer_content ) ?>,
4593 position : {
4594 edge : 'left',
4595 align: 'center'
4596 },
4597 buttons : function () {
4598 // Don't show pointer buttons.
4599 return '';
4600 },
4601 pointerWidth: 482
4602 }, <?php echo $this->apply_filters( 'optin_pointer_options_json', '{}' ) ?>));
4603
4604 <?php
4605 echo $this->apply_filters( 'optin_pointer_execute', "
4606
4607 optin.pointer('open');
4608
4609 // Tag the opt-in pointer with custom class.
4610 $('.wp-pointer #fs_connect')
4611 .parents('.wp-pointer.wp-pointer-top')
4612 .addClass('fs-opt-in-pointer');
4613
4614 ", 'element', 'optin' ) ?>
4615 }
4616 }
4617 });
4618 // ]]></script>
4619 <?php
4620 }
4621
4622 /**
4623 * Return current page's URL.
4624 *
4625 * @author Vova Feldman (@svovaf)
4626 * @since 1.0.7
4627 *
4628 * @return string
4629 */
4630 function current_page_url() {
4631 $url = 'http';
4632
4633 if ( isset( $_SERVER["HTTPS"] ) ) {
4634 if ( $_SERVER["HTTPS"] == "on" ) {
4635 $url .= "s";
4636 }
4637 }
4638 $url .= "://";
4639 if ( $_SERVER["SERVER_PORT"] != "80" ) {
4640 $url .= $_SERVER["SERVER_NAME"] . ":" . $_SERVER["SERVER_PORT"] . $_SERVER["REQUEST_URI"];
4641 } else {
4642 $url .= $_SERVER["SERVER_NAME"] . $_SERVER["REQUEST_URI"];
4643 }
4644
4645 return esc_url( $url );
4646 }
4647
4648 /**
4649 * Check if the current page is the plugin's main admin settings page.
4650 *
4651 * @author Vova Feldman (@svovaf)
4652 * @since 1.0.7
4653 *
4654 * @return bool
4655 */
4656 function _is_plugin_page() {
4657 return fs_is_plugin_page( $this->_menu->get_raw_slug() ) ||
4658 fs_is_plugin_page( $this->_slug );
4659 }
4660
4661 /* Events
4662 ------------------------------------------------------------------------------------------------------------------*/
4663 /**
4664 * Delete site install from Database.
4665 *
4666 * @author Vova Feldman (@svovaf)
4667 * @since 1.0.1
4668 *
4669 * @param bool $store
4670 */
4671 function _delete_site( $store = true ) {
4672 self::_delete_site_by_slug( $this->_slug, $this->_module_type, $store );
4673 }
4674
4675 /**
4676 * Delete site install from Database.
4677 *
4678 * @author Vova Feldman (@svovaf)
4679 * @since 1.2.2.7
4680 *
4681 * @param string $slug
4682 * @param string $module_type
4683 * @param bool $store
4684 */
4685 static function _delete_site_by_slug($slug, $module_type, $store = true ) {
4686 $sites = self::get_all_sites( $module_type );
4687
4688 if ( isset( $sites[ $slug ] ) ) {
4689 unset( $sites[ $slug ] );
4690 }
4691
4692 self::set_account_option_by_module( $module_type, 'sites', $sites, $store );
4693 }
4694
4695 /**
4696 * Delete plugin's plans information.
4697 *
4698 * @param bool $store Flush to Database if true.
4699 *
4700 * @author Vova Feldman (@svovaf)
4701 * @since 1.0.9
4702 */
4703 private function _delete_plans( $store = true ) {
4704 $this->_logger->entrance();
4705
4706 $plans = self::get_all_plans( $this->_module_type );
4707
4708 unset( $plans[ $this->_slug ] );
4709
4710 $this->set_account_option( 'plans', $plans, $store );
4711 }
4712
4713 /**
4714 * Delete all plugin licenses.
4715 *
4716 * @author Vova Feldman (@svovaf)
4717 * @since 1.0.9
4718 *
4719 * @param bool $store
4720 * @param string|bool $plugin_slug
4721 */
4722 private function _delete_licenses( $store = true, $plugin_slug = false ) {
4723 $this->_logger->entrance();
4724
4725 $all_licenses = self::get_all_licenses( $this->_module_type );
4726
4727 if ( ! is_string( $plugin_slug ) ) {
4728 $plugin_slug = $this->_slug;
4729 }
4730
4731 unset( $all_licenses[ $plugin_slug ] );
4732
4733 $this->set_account_option( 'licenses', $all_licenses, $store );
4734 }
4735
4736 /**
4737 * Check if Freemius was added on new plugin installation.
4738 *
4739 * @author Vova Feldman (@svovaf)
4740 * @since 1.1.5
4741 *
4742 * @return bool
4743 */
4744 function is_plugin_new_install() {
4745 return isset( $this->_storage->is_plugin_new_install ) &&
4746 $this->_storage->is_plugin_new_install;
4747 }
4748
4749 /**
4750 * Check if it's the first plugin release that is running Freemius.
4751 *
4752 * @author Vova Feldman (@svovaf)
4753 * @since 1.2.1.5
4754 *
4755 * @return bool
4756 */
4757 function is_first_freemius_powered_version() {
4758 return empty( $this->_storage->plugin_last_version );
4759 }
4760
4761 /**
4762 * @author Leo Fajardo (@leorw)
4763 * @since 1.2.2
4764 *
4765 * @return bool|string
4766 */
4767 private function get_previous_theme_slug() {
4768 return isset( $this->_storage->previous_theme ) ?
4769 $this->_storage->previous_theme :
4770 false;
4771 }
4772
4773 /**
4774 * @author Leo Fajardo (@leorw)
4775 * @since 1.2.2
4776 *
4777 * @return string
4778 */
4779 private function can_activate_previous_theme() {
4780 $slug = $this->get_previous_theme_slug();
4781 if ( false !== $slug && current_user_can( 'switch_themes' ) ) {
4782 $theme_instance = wp_get_theme( $slug );
4783
4784 return $theme_instance->exists();
4785 }
4786
4787 return false;
4788 }
4789
4790 /**
4791 * @author Leo Fajardo (@leorw)
4792 * @since 1.2.2
4793 *
4794 * @return string
4795 */
4796 private function activate_previous_theme() {
4797 switch_theme( $this->get_previous_theme_slug() );
4798 unset( $this->_storage->previous_theme );
4799
4800 global $pagenow;
4801 if ( 'themes.php' === $pagenow ) {
4802 /**
4803 * Refresh the active theme information.
4804 *
4805 * @author Leo Fajardo (@leorw)
4806 * @since 1.2.2
4807 */
4808 fs_redirect( admin_url( $pagenow ) );
4809 }
4810 }
4811
4812 /**
4813 * @author Leo Fajardo (@leorw)
4814 * @since 1.2.2
4815 *
4816 * @return string
4817 */
4818 function get_previous_theme_activation_url() {
4819 if ( ! $this->can_activate_previous_theme() ) {
4820 return '';
4821 }
4822
4823 /**
4824 * Activation URL
4825 *
4826 * @author Leo Fajardo (@leorw)
4827 * @since 1.2.2
4828 */
4829 return wp_nonce_url(
4830 admin_url( 'themes.php?action=activate&stylesheet=' . urlencode( $this->get_previous_theme_slug() ) ),
4831 'switch-theme_' . $this->get_previous_theme_slug()
4832 );
4833 }
4834
4835 /**
4836 * Saves the slug of the previous theme if it still exists so that it can be used by the logic in the opt-in
4837 * form that decides whether to add a close button to the opt-in dialog or not. So after a premium-only theme is
4838 * activated, the close button will appear and will reactivate the previous theme if clicked. If the previous
4839 * theme doesn't exist, then there will be no close button.
4840 *
4841 * @author Leo Fajardo (@leorw)
4842 * @since 1.2.2
4843 *
4844 * @param string $slug_or_name Old theme's slug or name.
4845 * @param bool|WP_Theme $old_theme WP_Theme instance of the old theme if it still exists.
4846 */
4847 function _activate_theme_event_hook( $slug_or_name, $old_theme = false ) {
4848 $this->_storage->previous_theme = ( false !== $old_theme ) ?
4849 $old_theme->get_stylesheet() :
4850 $slug_or_name;
4851
4852 $this->_activate_plugin_event_hook();
4853 }
4854
4855 /**
4856 * Plugin activated hook.
4857 *
4858 * @author Vova Feldman (@svovaf)
4859 * @since 1.0.1
4860 *
4861 * @uses FS_Api
4862 */
4863 function _activate_plugin_event_hook() {
4864 $this->_logger->entrance( 'slug = ' . $this->_slug );
4865
4866 if ( ! $this->is_user_admin() ) {
4867 return;
4868 }
4869
4870 $this->unregister_uninstall_hook();
4871
4872 // Clear API cache on activation.
4873 FS_Api::clear_cache();
4874
4875 $is_premium_version_activation = ( current_filter() !== ( 'activate_' . $this->_free_plugin_basename ) );
4876
4877 $this->_logger->info( 'Activating ' . ( $is_premium_version_activation ? 'premium' : 'free' ) . ' plugin version.' );
4878
4879 // 1. If running in the activation of the FREE module, get the basename of the PREMIUM.
4880 // 2. If running in the activation of the PREMIUM module, get the basename of the FREE.
4881 $other_version_basename = $is_premium_version_activation ?
4882 $this->_free_plugin_basename :
4883 $this->premium_plugin_basename();
4884
4885 /**
4886 * If the other module version is activate, deactivate it.
4887 *
4888 * @author Leo Fajardo (@leorw)
4889 * @since 1.2.2
4890 */
4891 if ( is_plugin_active( $other_version_basename ) ) {
4892 deactivate_plugins( $other_version_basename );
4893 }
4894
4895 if ( $this->is_registered() ) {
4896 if ( $is_premium_version_activation ) {
4897 $this->reconnect_locally();
4898 }
4899
4900
4901 // Schedule re-activation event and sync.
4902 // $this->sync_install( array(), true );
4903 $this->schedule_install_sync();
4904
4905 // If activating the premium module version, add an admin notice to congratulate for an upgrade completion.
4906 if ( $is_premium_version_activation ) {
4907 $this->_admin_notices->add(
4908 sprintf( $this->get_text_inline( 'The upgrade of %s was successfully completed.', 'successful-version-upgrade-message' ), sprintf( '<b>%s</b>', $this->_plugin->title ) ),
4909 $this->get_text_x_inline( 'W00t',
4910 'Used to express elation, enthusiasm, or triumph (especially in electronic communication).', 'woot' ) . '!'
4911 );
4912 }
4913 } else if ( $this->is_anonymous() ) {
4914 /**
4915 * Reset "skipped" click cache on the following:
4916 * 1. Freemius DEV mode.
4917 * 2. WordPress DEBUG mode.
4918 * 3. If a plugin and the user skipped the exact same version before.
4919 *
4920 * @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).
4921 *
4922 * @todo 4. If explicitly asked to retry after every activation.
4923 */
4924 if ( WP_FS__DEV_MODE ||
4925 (
4926 ( $this->is_plugin() || ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ) &&
4927 $this->get_plugin_version() == $this->_storage->is_anonymous['version']
4928 )
4929 ) {
4930 $this->reset_anonymous_mode();
4931 }
4932 }
4933
4934 if ( ! isset( $this->_storage->is_plugin_new_install ) ) {
4935 /**
4936 * If no previous version of plugin's version exist, it means that it's either
4937 * the first time that the plugin installed on the site, or the plugin was installed
4938 * before but didn't have Freemius integrated.
4939 *
4940 * Since register_activation_hook() do NOT fires on updates since 3.1, and only fires
4941 * on manual activation via the dashboard, is_plugin_activation() is TRUE
4942 * only after immediate activation.
4943 *
4944 * @since 1.1.4
4945 * @link https://make.wordpress.org/core/2010/10/27/plugin-activation-hooks-no-longer-fire-for-updates/
4946 */
4947 $this->_storage->is_plugin_new_install = empty( $this->_storage->plugin_last_version );
4948 }
4949
4950 if ( ! $this->_anonymous_mode &&
4951 $this->has_api_connectivity( WP_FS__DEV_MODE ) &&
4952 ! $this->_isAutoInstall
4953 ) {
4954 // Store hint that the plugin was just activated to enable auto-redirection to settings.
4955 add_option( 'fs_'
4956 . ( $this->is_plugin() ? '' : $this->_module_type . '_' )
4957 . "{$this->_slug}_activated", true );
4958 }
4959
4960 /**
4961 * Activation hook is executed after the plugin's main file is loaded, therefore,
4962 * after the plugin was loaded. The logic is located at activate_plugin()
4963 * ./wp-admin/includes/plugin.php.
4964 *
4965 * @author Vova Feldman (@svovaf)
4966 * @since 1.1.9
4967 */
4968 $this->_storage->was_plugin_loaded = true;
4969 }
4970
4971 /**
4972 * Delete account.
4973 *
4974 * @author Vova Feldman (@svovaf)
4975 * @since 1.0.3
4976 *
4977 * @param bool $check_user Enforce checking if user have plugins activation privileges.
4978 */
4979 function delete_account_event( $check_user = true ) {
4980 $this->_logger->entrance( 'slug = ' . $this->_slug );
4981
4982 if ( $check_user && ! $this->is_user_admin() ) {
4983 return;
4984 }
4985
4986 $this->do_action( 'before_account_delete' );
4987
4988 // Clear all admin notices.
4989 $this->_admin_notices->clear_all_sticky();
4990
4991 $this->_delete_site( false );
4992
4993 $this->_delete_plans( false );
4994
4995 $this->_delete_licenses( false );
4996
4997 // Delete add-ons related to plugin's account.
4998 $this->_delete_account_addons( false );
4999
5000 // @todo Delete plans and licenses of add-ons.
5001
5002 self::$_accounts->store();
5003
5004 /**
5005 * IMPORTANT:
5006 * Clear crons must be executed before clearing all storage.
5007 * Otherwise, the cron will not be cleared.
5008 */
5009 $this->clear_sync_cron();
5010 $this->clear_install_sync_cron();
5011
5012 // Clear all storage data.
5013 $this->_storage->clear_all( true, array(
5014 'connectivity_test',
5015 'is_on',
5016 ) );
5017
5018 // Send delete event.
5019 $this->get_api_site_scope()->call( '/', 'delete' );
5020
5021 $this->do_action( 'after_account_delete' );
5022 }
5023
5024 /**
5025 * Plugin deactivation hook.
5026 *
5027 * @author Vova Feldman (@svovaf)
5028 * @since 1.0.1
5029 */
5030 function _deactivate_plugin_hook() {
5031 $this->_logger->entrance( 'slug = ' . $this->_slug );
5032
5033 if ( ! current_user_can( 'activate_plugins' ) ) {
5034 return;
5035 }
5036
5037 $this->_admin_notices->clear_all_sticky();
5038 if ( isset( $this->_storage->sticky_optin_added ) ) {
5039 unset( $this->_storage->sticky_optin_added );
5040 }
5041
5042 if ( ! isset( $this->_storage->is_plugin_new_install ) ) {
5043 // Remember that plugin was already installed.
5044 $this->_storage->is_plugin_new_install = false;
5045 }
5046
5047 // Hook to plugin uninstall.
5048 register_uninstall_hook( $this->_plugin_main_file_path, array( 'Freemius', '_uninstall_plugin_hook' ) );
5049
5050 $this->clear_module_main_file_cache();
5051 $this->clear_sync_cron();
5052 $this->clear_install_sync_cron();
5053
5054 if ( $this->is_registered() ) {
5055 // Send deactivation event.
5056 $this->sync_install( array(
5057 'is_active' => false,
5058 ) );
5059 } else {
5060 if ( ! $this->has_api_connectivity() ) {
5061 // Reset connectivity test cache.
5062 unset( $this->_storage->connectivity_test );
5063 }
5064 }
5065
5066 // Clear API cache on deactivation.
5067 FS_Api::clear_cache();
5068
5069 $this->remove_sdk_reference();
5070 }
5071
5072 /**
5073 * @author Vova Feldman (@svovaf)
5074 * @since 1.1.6
5075 */
5076 private function remove_sdk_reference() {
5077 global $fs_active_plugins;
5078
5079 foreach ( $fs_active_plugins->plugins as $sdk_path => &$data ) {
5080 if ( $this->_plugin_basename == $data->plugin_path ) {
5081 unset( $fs_active_plugins->plugins[ $sdk_path ] );
5082 break;
5083 }
5084 }
5085
5086 fs_fallback_to_newest_active_sdk();
5087 }
5088
5089 /**
5090 * @author Vova Feldman (@svovaf)
5091 * @since 1.1.3
5092 *
5093 * @param bool $is_anonymous
5094 */
5095 private function set_anonymous_mode( $is_anonymous = true ) {
5096 // Store information regarding skip to try and opt-in the user
5097 // again in the future.
5098 $this->_storage->is_anonymous = array(
5099 'is' => $is_anonymous,
5100 'timestamp' => WP_FS__SCRIPT_START_TIME,
5101 'version' => $this->get_plugin_version(),
5102 );
5103
5104 // Update anonymous mode cache.
5105 $this->_is_anonymous = $is_anonymous;
5106 }
5107
5108 /**
5109 * @author Vova Feldman (@svovaf)
5110 * @since 1.1.3
5111 */
5112 private function reset_anonymous_mode() {
5113 unset( $this->_storage->is_anonymous );
5114
5115 /**
5116 * Ensure that this field is also "false", otherwise, if the current module's type is "theme" and the module
5117 * has no menus, the opt-in popup will not be shown immediately (in this case, the user will have to click
5118 * on the admin notice that contains the opt-in link in order to trigger the opt-in popup).
5119 *
5120 * @author Leo Fajardo (@leorw)
5121 * @since 1.2.2
5122 */
5123 unset( $this->_is_anonymous );
5124 }
5125
5126 /**
5127 * Clears the anonymous mode and redirects to the opt-in screen.
5128 *
5129 * @author Vova Feldman (@svovaf)
5130 * @since 1.1.7
5131 */
5132 function connect_again() {
5133 if ( ! $this->is_anonymous() ) {
5134 return;
5135 }
5136
5137 $this->reset_anonymous_mode();
5138
5139 fs_redirect( $this->get_activation_url() );
5140 }
5141
5142 /**
5143 * Skip account connect, and set anonymous mode.
5144 *
5145 * @author Vova Feldman (@svovaf)
5146 * @since 1.1.1
5147 */
5148 private function skip_connection() {
5149 $this->_logger->entrance();
5150
5151 $this->_admin_notices->remove_sticky( 'connect_account' );
5152
5153 $this->set_anonymous_mode();
5154
5155 // Send anonymous skip event.
5156 // No user identified info nor any tracking will be sent after the user skips the opt-in.
5157 $this->get_api_plugin_scope()->call( 'skip.json', 'put', array(
5158 'uid' => $this->get_anonymous_id(),
5159 ) );
5160 }
5161
5162 /**
5163 * Plugin version update hook.
5164 *
5165 * @author Vova Feldman (@svovaf)
5166 * @since 1.0.4
5167 */
5168 private function update_plugin_version_event() {
5169 $this->_logger->entrance();
5170
5171 if ( ! $this->is_registered() ) {
5172 return;
5173 }
5174
5175 $this->schedule_install_sync();
5176 // $this->sync_install( array(), true );
5177 }
5178
5179 /**
5180 * Return a list of modified plugins since the last sync.
5181 *
5182 * Note:
5183 * There's no point to store a plugins counter since even if the number of
5184 * plugins didn't change, we still need to check if the versions are all the
5185 * same and the activity state is similar.
5186 *
5187 * @author Vova Feldman (@svovaf)
5188 * @since 1.1.8
5189 *
5190 * @return array|false
5191 */
5192 private function get_plugins_data_for_api() {
5193 // Alias.
5194 $option_name = 'all_plugins';
5195
5196 $all_cached_plugins = self::$_accounts->get_option( $option_name );
5197
5198 if ( ! is_object( $all_cached_plugins ) ) {
5199 $all_cached_plugins = (object) array(
5200 'timestamp' => '',
5201 'md5' => '',
5202 'plugins' => array(),
5203 );
5204 }
5205
5206 $time = time();
5207
5208 if ( ! empty( $all_cached_plugins->timestamp ) &&
5209 ( $time - $all_cached_plugins->timestamp ) < WP_FS__TIME_5_MIN_IN_SEC
5210 ) {
5211 // Don't send plugin updates if last update was in the past 5 min.
5212 return false;
5213 }
5214
5215 // Write timestamp to lock the logic.
5216 $all_cached_plugins->timestamp = $time;
5217 self::$_accounts->set_option( $option_name, $all_cached_plugins, true );
5218
5219 // Reload options from DB.
5220 self::$_accounts->load( true );
5221 $all_cached_plugins = self::$_accounts->get_option( $option_name );
5222
5223 if ( $time != $all_cached_plugins->timestamp ) {
5224 // If timestamp is different, then another thread captured the lock.
5225 return false;
5226 }
5227
5228 // Check if there's a change in plugins.
5229 $all_plugins = self::get_all_plugins();
5230
5231 // Check if plugins changed.
5232 ksort( $all_plugins );
5233
5234 $plugins_signature = '';
5235 foreach ( $all_plugins as $basename => $data ) {
5236 $plugins_signature .= $data['slug'] . ',' .
5237 $data['Version'] . ',' .
5238 ( $data['is_active'] ? '1' : '0' ) . ';';
5239 }
5240
5241 // Check if plugins status changed (version or active/inactive).
5242 $plugins_changed = ( $all_cached_plugins->md5 !== md5( $plugins_signature ) );
5243
5244 $plugins_update_data = array();
5245
5246 if ( $plugins_changed ) {
5247 // Change in plugins, report changes.
5248
5249 // Update existing plugins info.
5250 foreach ( $all_cached_plugins->plugins as $basename => $data ) {
5251 if ( ! isset( $all_plugins[ $basename ] ) ) {
5252 // Plugin uninstalled.
5253 $uninstalled_plugin_data = $data;
5254 $uninstalled_plugin_data['is_active'] = false;
5255 $uninstalled_plugin_data['is_uninstalled'] = true;
5256 $plugins_update_data[] = $uninstalled_plugin_data;
5257
5258 unset( $all_plugins[ $basename ] );
5259 unset( $all_cached_plugins->plugins[ $basename ] );
5260 } else if ( $data['is_active'] !== $all_plugins[ $basename ]['is_active'] ||
5261 $data['version'] !== $all_plugins[ $basename ]['Version']
5262 ) {
5263 // Plugin activated or deactivated, or version changed.
5264 $all_cached_plugins->plugins[ $basename ]['is_active'] = $all_plugins[ $basename ]['is_active'];
5265 $all_cached_plugins->plugins[ $basename ]['version'] = $all_plugins[ $basename ]['Version'];
5266
5267 $plugins_update_data[] = $all_cached_plugins->plugins[ $basename ];
5268 }
5269 }
5270
5271 // Find new plugins that weren't yet seen before.
5272 foreach ( $all_plugins as $basename => $data ) {
5273 if ( ! isset( $all_cached_plugins->plugins[ $basename ] ) ) {
5274 // New plugin.
5275 $new_plugin = array(
5276 'slug' => $data['slug'],
5277 'version' => $data['Version'],
5278 'title' => $data['Name'],
5279 'is_active' => $data['is_active'],
5280 'is_uninstalled' => false,
5281 );
5282
5283 $plugins_update_data[] = $new_plugin;
5284 $all_cached_plugins->plugins[ $basename ] = $new_plugin;
5285 }
5286 }
5287
5288 $all_cached_plugins->md5 = md5( $plugins_signature );
5289 $all_cached_plugins->timestamp = $time;
5290 self::$_accounts->set_option( $option_name, $all_cached_plugins, true );
5291 }
5292
5293 return $plugins_update_data;
5294 }
5295
5296 /**
5297 * Return a list of modified themes since the last sync.
5298 *
5299 * Note:
5300 * There's no point to store a themes counter since even if the number of
5301 * themes didn't change, we still need to check if the versions are all the
5302 * same and the activity state is similar.
5303 *
5304 * @author Vova Feldman (@svovaf)
5305 * @since 1.1.8
5306 *
5307 * @return array|false
5308 */
5309 private function get_themes_data_for_api() {
5310 // Alias.
5311 $option_name = 'all_themes';
5312
5313 $all_cached_themes = self::$_accounts->get_option( $option_name );
5314
5315 if ( ! is_object( $all_cached_themes ) ) {
5316 $all_cached_themes = (object) array(
5317 'timestamp' => '',
5318 'md5' => '',
5319 'themes' => array(),
5320 );
5321 }
5322
5323 $time = time();
5324
5325 if ( ! empty( $all_cached_themes->timestamp ) &&
5326 ( $time - $all_cached_themes->timestamp ) < WP_FS__TIME_5_MIN_IN_SEC
5327 ) {
5328 // Don't send theme updates if last update was in the past 5 min.
5329 return false;
5330 }
5331
5332 // Write timestamp to lock the logic.
5333 $all_cached_themes->timestamp = $time;
5334 self::$_accounts->set_option( $option_name, $all_cached_themes, true );
5335
5336 // Reload options from DB.
5337 self::$_accounts->load( true );
5338 $all_cached_themes = self::$_accounts->get_option( $option_name );
5339
5340 if ( $time != $all_cached_themes->timestamp ) {
5341 // If timestamp is different, then another thread captured the lock.
5342 return false;
5343 }
5344
5345 // Get active theme.
5346 $active_theme = wp_get_theme();
5347 $active_theme_stylesheet = $active_theme->get_stylesheet();
5348
5349 // Check if there's a change in themes.
5350 $all_themes = wp_get_themes();
5351
5352 // Check if themes changed.
5353 ksort( $all_themes );
5354
5355 $themes_signature = '';
5356 foreach ( $all_themes as $slug => $data ) {
5357 $is_active = ( $slug === $active_theme_stylesheet );
5358 $themes_signature .= $slug . ',' .
5359 $data->version . ',' .
5360 ( $is_active ? '1' : '0' ) . ';';
5361 }
5362
5363 // Check if themes status changed (version or active/inactive).
5364 $themes_changed = ( $all_cached_themes->md5 !== md5( $themes_signature ) );
5365
5366 $themes_update_data = array();
5367
5368 if ( $themes_changed ) {
5369 // Change in themes, report changes.
5370
5371 // Update existing themes info.
5372 foreach ( $all_cached_themes->themes as $slug => $data ) {
5373 $is_active = ( $slug === $active_theme_stylesheet );
5374
5375 if ( ! isset( $all_themes[ $slug ] ) ) {
5376 // Plugin uninstalled.
5377 $uninstalled_theme_data = $data;
5378 $uninstalled_theme_data['is_active'] = false;
5379 $uninstalled_theme_data['is_uninstalled'] = true;
5380 $themes_update_data[] = $uninstalled_theme_data;
5381
5382 unset( $all_themes[ $slug ] );
5383 unset( $all_cached_themes->themes[ $slug ] );
5384 } else if ( $data['is_active'] !== $is_active ||
5385 $data['version'] !== $all_themes[ $slug ]->version
5386 ) {
5387 // Plugin activated or deactivated, or version changed.
5388
5389 $all_cached_themes->themes[ $slug ]['is_active'] = $is_active;
5390 $all_cached_themes->themes[ $slug ]['version'] = $all_themes[ $slug ]->version;
5391
5392 $themes_update_data[] = $all_cached_themes->themes[ $slug ];
5393 }
5394 }
5395
5396 // Find new themes that weren't yet seen before.
5397 foreach ( $all_themes as $slug => $data ) {
5398 if ( ! isset( $all_cached_themes->themes[ $slug ] ) ) {
5399 $is_active = ( $slug === $active_theme_stylesheet );
5400
5401 // New plugin.
5402 $new_plugin = array(
5403 'slug' => $slug,
5404 'version' => $data->version,
5405 'title' => $data->name,
5406 'is_active' => $is_active,
5407 'is_uninstalled' => false,
5408 );
5409
5410 $themes_update_data[] = $new_plugin;
5411 $all_cached_themes->themes[ $slug ] = $new_plugin;
5412 }
5413 }
5414
5415 $all_cached_themes->md5 = md5( $themes_signature );
5416 $all_cached_themes->timestamp = time();
5417 self::$_accounts->set_option( $option_name, $all_cached_themes, true );
5418 }
5419
5420 return $themes_update_data;
5421 }
5422
5423 /**
5424 * Update install details.
5425 *
5426 * @author Vova Feldman (@svovaf)
5427 * @since 1.1.2
5428 *
5429 * @param string[] string $override
5430 * @param bool $include_plugins Since 1.1.8 by default include plugin changes.
5431 * @param bool $include_themes Since 1.1.8 by default include plugin changes.
5432 *
5433 * @return array
5434 */
5435 private function get_install_data_for_api(
5436 array $override,
5437 $include_plugins = true,
5438 $include_themes = true
5439 ) {
5440 /**
5441 * @since 1.1.8 Also send plugin updates.
5442 */
5443 if ( $include_plugins && ! isset( $override['plugins'] ) ) {
5444 $plugins = $this->get_plugins_data_for_api();
5445 if ( ! empty( $plugins ) ) {
5446 $override['plugins'] = $plugins;
5447 }
5448 }
5449 /**
5450 * @since 1.1.8 Also send themes updates.
5451 */
5452 if ( $include_themes && ! isset( $override['themes'] ) ) {
5453 $themes = $this->get_themes_data_for_api();
5454 if ( ! empty( $themes ) ) {
5455 $override['themes'] = $themes;
5456 }
5457 }
5458
5459 return array_merge( array(
5460 'version' => $this->get_plugin_version(),
5461 'is_premium' => $this->is_premium(),
5462 'language' => get_bloginfo( 'language' ),
5463 'charset' => get_bloginfo( 'charset' ),
5464 'platform_version' => get_bloginfo( 'version' ),
5465 'sdk_version' => $this->version,
5466 'programming_language_version' => phpversion(),
5467 'title' => get_bloginfo( 'name' ),
5468 'url' => get_site_url(),
5469 // Special params.
5470 'is_active' => true,
5471 'is_disconnected' => $this->is_tracking_prohibited(),
5472 'is_uninstalled' => false,
5473 ), $override );
5474 }
5475
5476 /**
5477 * Update install only if changed.
5478 *
5479 * @author Vova Feldman (@svovaf)
5480 * @since 1.0.9
5481 *
5482 * @param string[] string $override
5483 * @param bool $flush
5484 *
5485 * @return false|object|string
5486 */
5487 private function send_install_update( $override = array(), $flush = false ) {
5488 $this->_logger->entrance();
5489
5490 $check_properties = $this->get_install_data_for_api( $override );
5491
5492 if ( $flush ) {
5493 $params = $check_properties;
5494 } else {
5495 $params = array();
5496 $special = array();
5497 $special_override = false;
5498
5499 foreach ( $check_properties as $p => $v ) {
5500 if ( property_exists( $this->_site, $p ) ) {
5501 if ( ( is_bool( $this->_site->{$p} ) || ! empty( $this->_site->{$p} ) ) &&
5502 $this->_site->{$p} != $v
5503 ) {
5504 $this->_site->{$p} = $v;
5505 $params[ $p ] = $v;
5506 }
5507 } else {
5508 $special[ $p ] = $v;
5509
5510 if ( isset( $override[ $p ] ) ||
5511 'plugins' === $p ||
5512 'themes' === $p
5513 ) {
5514 $special_override = true;
5515 }
5516 }
5517 }
5518
5519 if ( $special_override || 0 < count( $params ) ) {
5520 // Add special params only if has at least one
5521 // standard param, or if explicitly requested to
5522 // override a special param or a param which is not exist
5523 // in the install object.
5524 $params = array_merge( $params, $special );
5525 }
5526 }
5527
5528 if ( 0 < count( $params ) ) {
5529 // Update last install sync timestamp.
5530 $this->_storage->install_sync_timestamp = time();
5531
5532 $params['uid'] = $this->get_anonymous_id();
5533
5534 // Send updated values to FS.
5535 $site = $this->get_api_site_scope()->call( '/', 'put', $params );
5536
5537 if ( $this->is_api_result_entity( $site ) ) {
5538 // I successfully sent install update, clear scheduled sync if exist.
5539 $this->clear_install_sync_cron();
5540 }
5541
5542 return $site;
5543 }
5544
5545 return false;
5546 }
5547
5548 /**
5549 * Update install only if changed.
5550 *
5551 * @author Vova Feldman (@svovaf)
5552 * @since 1.0.9
5553 *
5554 * @param string[] string $override
5555 * @param bool $flush
5556 */
5557 private function sync_install( $override = array(), $flush = false ) {
5558 $this->_logger->entrance();
5559
5560 $site = $this->send_install_update( $override, $flush );
5561
5562 if ( false === $site ) {
5563 // No sync required.
5564 return;
5565 }
5566
5567 if ( ! $this->is_api_result_entity( $site ) ) {
5568 // Failed to sync, don't update locally.
5569 return;
5570 }
5571
5572 $plan = $this->get_plan();
5573 $this->_site = new FS_Site( $site );
5574 $this->_site->plan = $plan;
5575
5576 $this->_store_site( true );
5577 }
5578
5579 /**
5580 * Track install's custom event.
5581 *
5582 * IMPORTANT:
5583 * Custom event tracking is currently only supported for specific clients.
5584 * If you are not one of them, please don't use this method. If you will,
5585 * the API will simply ignore your request based on the plugin ID.
5586 *
5587 * Need custom tracking for your plugin or theme?
5588 * If you are interested in custom event tracking please contact yo@freemius.com
5589 * for further details.
5590 *
5591 * @author Vova Feldman (@svovaf)
5592 * @since 1.2.1
5593 *
5594 * @param string $name Event name.
5595 * @param array $properties Associative key/value array with primitive values only
5596 * @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
5597 * @param bool $once If true, event will be tracked only once. IMPORTANT: Still trigger the API call.
5598 *
5599 * @return object|false Event data or FALSE on failure.
5600 *
5601 * @throws \Freemius_InvalidArgumentException
5602 */
5603 public function track_event( $name, $properties = array(), $process_at = false, $once = false ) {
5604 $this->_logger->entrance( http_build_query( array( 'name' => $name, 'once' => $once ) ) );
5605
5606 if ( ! $this->is_registered() ) {
5607 return false;
5608 }
5609
5610 $event = array( 'type' => $name );
5611
5612 if ( is_numeric( $process_at ) && $process_at > time() ) {
5613 $event['process_at'] = $process_at;
5614 }
5615
5616 if ( $once ) {
5617 $event['once'] = true;
5618 }
5619
5620 if ( ! empty( $properties ) ) {
5621 // Verify associative array values are primitive.
5622 foreach ( $properties as $k => $v ) {
5623 if ( ! is_scalar( $v ) ) {
5624 throw new Freemius_InvalidArgumentException( 'The $properties argument must be an associative key/value array with primitive values only.' );
5625 }
5626 }
5627
5628 $event['properties'] = $properties;
5629 }
5630
5631 $result = $this->get_api_site_scope()->call( 'events.json', 'post', $event );
5632
5633 return $this->is_api_error( $result ) ?
5634 false :
5635 $result;
5636 }
5637
5638 /**
5639 * Track install's custom event only once, but it still triggers the API call.
5640 *
5641 * IMPORTANT:
5642 * Custom event tracking is currently only supported for specific clients.
5643 * If you are not one of them, please don't use this method. If you will,
5644 * the API will simply ignore your request based on the plugin ID.
5645 *
5646 * Need custom tracking for your plugin or theme?
5647 * If you are interested in custom event tracking please contact yo@freemius.com
5648 * for further details.
5649 *
5650 * @author Vova Feldman (@svovaf)
5651 * @since 1.2.1
5652 *
5653 * @param string $name Event name.
5654 * @param array $properties Associative key/value array with primitive values only
5655 * @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
5656 *
5657 * @return object|false Event data or FALSE on failure.
5658 *
5659 * @throws \Freemius_InvalidArgumentException
5660 *
5661 * @user Freemius::track_event()
5662 */
5663 public function track_event_once( $name, $properties = array(), $process_at = false ) {
5664 return $this->track_event( $name, $properties, $process_at, true );
5665 }
5666
5667 /**
5668 * Plugin uninstall hook.
5669 *
5670 * @author Vova Feldman (@svovaf)
5671 * @since 1.0.1
5672 *
5673 * @param bool $check_user Enforce checking if user have plugins activation privileges.
5674 */
5675 function _uninstall_plugin_event( $check_user = true ) {
5676 $this->_logger->entrance( 'slug = ' . $this->_slug );
5677
5678 if ( $check_user && ! current_user_can( 'activate_plugins' ) ) {
5679 return;
5680 }
5681
5682 $params = array();
5683 $uninstall_reason = null;
5684 if ( isset( $this->_storage->uninstall_reason ) ) {
5685 $uninstall_reason = $this->_storage->uninstall_reason;
5686 $params['reason_id'] = $uninstall_reason->id;
5687 $params['reason_info'] = $uninstall_reason->info;
5688 }
5689
5690 if ( ! $this->is_registered() ) {
5691 // Send anonymous uninstall event only if user submitted a feedback.
5692 if ( isset( $uninstall_reason ) ) {
5693 if ( isset( $uninstall_reason->is_anonymous ) && ! $uninstall_reason->is_anonymous ) {
5694 $this->opt_in( false, false, false, false, true );
5695 } else {
5696 $params['uid'] = $this->get_anonymous_id();
5697 $this->get_api_plugin_scope()->call( 'uninstall.json', 'put', $params );
5698 }
5699 }
5700 } else {
5701 // Send uninstall event.
5702 $this->send_install_update( array_merge( $params, array(
5703 'is_active' => false,
5704 'is_uninstalled' => true,
5705 ) ) );
5706 }
5707
5708 // @todo Decide if we want to delete plugin information from db.
5709 }
5710
5711 /**
5712 * @author Vova Feldman (@svovaf)
5713 * @since 1.1.1
5714 *
5715 * @return string
5716 */
5717 function premium_plugin_basename() {
5718 return "{$this->_slug}-premium/" . basename( $this->_free_plugin_basename );
5719 }
5720
5721 /**
5722 * Uninstall plugin hook. Called only when connected his account with Freemius for active sites tracking.
5723 *
5724 * @author Vova Feldman (@svovaf)
5725 * @since 1.0.2
5726 */
5727 public static function _uninstall_plugin_hook() {
5728 self::_load_required_static();
5729
5730 self::$_static_logger->entrance();
5731
5732 if ( ! current_user_can( 'activate_plugins' ) ) {
5733 return;
5734 }
5735
5736 $plugin_file = substr( current_filter(), strlen( 'uninstall_' ) );
5737
5738 self::$_static_logger->info( 'plugin = ' . $plugin_file );
5739
5740 define( 'WP_FS__UNINSTALL_MODE', true );
5741
5742 $fs = self::get_instance_by_file( $plugin_file );
5743
5744 if ( is_object( $fs ) ) {
5745 self::require_plugin_essentials();
5746
5747 if ( is_plugin_active( $fs->_free_plugin_basename ) ||
5748 is_plugin_active( $fs->premium_plugin_basename() )
5749 ) {
5750 // Deleting Free or Premium plugin version while the other version still installed.
5751 return;
5752 }
5753
5754 $fs->_uninstall_plugin_event();
5755
5756 $fs->do_action( 'after_uninstall' );
5757 }
5758 }
5759
5760 #----------------------------------------------------------------------------------
5761 #region Plugin Information
5762 #----------------------------------------------------------------------------------
5763
5764 /**
5765 * Load WordPress core plugin.php essential module.
5766 *
5767 * @author Vova Feldman (@svovaf)
5768 * @since 1.1.1
5769 */
5770 private static function require_plugin_essentials() {
5771 if ( ! function_exists( 'get_plugins' ) ) {
5772 self::$_static_logger->log( 'Including wp-admin/includes/plugin.php...' );
5773
5774 require_once ABSPATH . 'wp-admin/includes/plugin.php';
5775 }
5776 }
5777
5778 /**
5779 * Load WordPress core pluggable.php module.
5780 *
5781 * @author Vova Feldman (@svovaf)
5782 * @since 1.1.2
5783 */
5784 private static function require_pluggable_essentials() {
5785 if ( ! function_exists( 'wp_get_current_user' ) ) {
5786 require_once ABSPATH . 'wp-includes/pluggable.php';
5787 }
5788 }
5789
5790 /**
5791 * Return plugin data.
5792 *
5793 * @author Vova Feldman (@svovaf)
5794 * @since 1.0.1
5795 *
5796 * @return array
5797 */
5798 function get_plugin_data() {
5799 if ( ! isset( $this->_plugin_data ) ) {
5800 self::require_plugin_essentials();
5801
5802 if ( $this->is_plugin() ) {
5803 /**
5804 * @author Vova Feldman (@svovaf)
5805 * @since 1.2.0 When using get_plugin_data() do NOT translate plugin data.
5806 *
5807 * @link https://github.com/Freemius/wordpress-sdk/issues/77
5808 */
5809 $plugin_data = get_plugin_data(
5810 $this->_plugin_main_file_path,
5811 false,
5812 false
5813 );
5814 } else {
5815 $theme_data = wp_get_theme();
5816
5817 $plugin_data = array(
5818 'Name' => $theme_data->get( 'Name' ),
5819 'Version' => $theme_data->get( 'Version' ),
5820 'Author' => $theme_data->get( 'Author' ),
5821 'Description' => $theme_data->get( 'Description' ),
5822 'PluginURI' => $theme_data->get( 'ThemeURI' ),
5823 );
5824 }
5825
5826 $this->_plugin_data = $plugin_data;
5827 }
5828
5829 return $this->_plugin_data;
5830 }
5831
5832 /**
5833 * @author Vova Feldman (@svovaf)
5834 * @since 1.0.1
5835 * @since 1.2.2.5 If slug not set load slug by module ID.
5836 *
5837 * @return string Plugin slug.
5838 */
5839 function get_slug() {
5840 if ( ! isset( $this->_slug ) ) {
5841 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
5842 $this->_slug = $id_slug_type_path_map[ $this->_module_id ]['slug'];
5843 }
5844
5845 return $this->_slug;
5846 }
5847
5848 /**
5849 * @author Vova Feldman (@svovaf)
5850 * @since 1.2.1.7
5851 *
5852 * @return string Plugin slug.
5853 */
5854 function get_target_folder_name() {
5855 return $this->_slug . ( $this->can_use_premium_code() ? '-premium' : '' );
5856 }
5857
5858 /**
5859 * @author Vova Feldman (@svovaf)
5860 * @since 1.0.1
5861 *
5862 * @return number Plugin ID.
5863 */
5864 function get_id() {
5865 return $this->_plugin->id;
5866 }
5867
5868 /**
5869 * @author Vova Feldman (@svovaf)
5870 * @since 1.2.1.5
5871 *
5872 * @return string Freemius SDK version
5873 */
5874 function get_sdk_version() {
5875 return $this->version;
5876 }
5877
5878 /**
5879 * @author Vova Feldman (@svovaf)
5880 * @since 1.2.1.5
5881 *
5882 * @return number Parent plugin ID (if parent exist).
5883 */
5884 function get_parent_id() {
5885 return $this->is_addon() ?
5886 $this->get_parent_instance()->get_id() :
5887 $this->_plugin->id;
5888 }
5889
5890 /**
5891 * @author Vova Feldman (@svovaf)
5892 * @since 1.0.1
5893 *
5894 * @return string Plugin public key.
5895 */
5896 function get_public_key() {
5897 return $this->_plugin->public_key;
5898 }
5899
5900 /**
5901 * Will be available only on sandbox mode.
5902 *
5903 * @author Vova Feldman (@svovaf)
5904 * @since 1.0.4
5905 *
5906 * @return mixed Plugin secret key.
5907 */
5908 function get_secret_key() {
5909 return $this->_plugin->secret_key;
5910 }
5911
5912 /**
5913 * @author Vova Feldman (@svovaf)
5914 * @since 1.1.1
5915 *
5916 * @return bool
5917 */
5918 function has_secret_key() {
5919 return ! empty( $this->_plugin->secret_key );
5920 }
5921
5922 /**
5923 * @author Vova Feldman (@svovaf)
5924 * @since 1.0.9
5925 *
5926 * @return string
5927 */
5928 function get_plugin_name() {
5929 $this->_logger->entrance();
5930
5931 if ( ! isset( $this->_plugin_name ) ) {
5932 $plugin_data = $this->get_plugin_data();
5933
5934 // Get name.
5935 $this->_plugin_name = $plugin_data['Name'];
5936
5937 // Check if plugin name contains "(Premium)" suffix and remove it.
5938 $suffix = ' (premium)';
5939 $suffix_len = strlen( $suffix );
5940
5941 if ( strlen( $plugin_data['Name'] ) > $suffix_len &&
5942 $suffix === substr( strtolower( $plugin_data['Name'] ), - $suffix_len )
5943 ) {
5944 $this->_plugin_name = substr( $plugin_data['Name'], 0, - $suffix_len );
5945 }
5946
5947 $this->_logger->departure( 'Name = ' . $this->_plugin_name );
5948 }
5949
5950 return $this->_plugin_name;
5951 }
5952
5953 /**
5954 * @author Vova Feldman (@svovaf)
5955 * @since 1.0.0
5956 *
5957 * @return string
5958 */
5959 function get_plugin_version() {
5960 $this->_logger->entrance();
5961
5962 $plugin_data = $this->get_plugin_data();
5963
5964 $this->_logger->departure( 'Version = ' . $plugin_data['Version'] );
5965
5966 return $this->apply_filters( 'plugin_version', $plugin_data['Version'] );
5967 }
5968
5969 /**
5970 * @author Vova Feldman (@svovaf)
5971 * @since 1.2.1.7
5972 *
5973 * @return string
5974 */
5975 function get_plugin_title() {
5976 $this->_logger->entrance();
5977
5978 $title = $this->_plugin->title;
5979
5980 return $this->apply_filters( 'plugin_title', $title );
5981 }
5982
5983 /**
5984 * @author Vova Feldman (@svovaf)
5985 * @since 1.2.2.7
5986 *
5987 * @param bool $lowercase
5988 *
5989 * @return string
5990 */
5991 function get_module_label( $lowercase = false ) {
5992 $label = $this->is_addon() ?
5993 $this->get_text_inline( 'Add-On', 'addon' ) :
5994 ( $this->is_plugin() ?
5995 $this->get_text_inline( 'Plugin', 'plugin' ) :
5996 $this->get_text_inline( 'Theme', 'theme' ) );
5997
5998 if ( $lowercase ) {
5999 $label = strtolower( $label );
6000 }
6001
6002 return $label;
6003 }
6004
6005 /**
6006 * @author Vova Feldman (@svovaf)
6007 * @since 1.0.4
6008 *
6009 * @return string
6010 */
6011 function get_plugin_basename() {
6012 if ( ! isset( $this->_plugin_basename ) ) {
6013 if ( $this->is_plugin() ) {
6014 $this->_plugin_basename = plugin_basename( $this->_plugin_main_file_path );
6015 } else {
6016 $this->_plugin_basename = basename( dirname( $this->_plugin_main_file_path ) );
6017 }
6018 }
6019
6020 return $this->_plugin_basename;
6021 }
6022
6023 function get_plugin_folder_name() {
6024 $this->_logger->entrance();
6025
6026 $plugin_folder = $this->_plugin_basename;
6027
6028 while ( '.' !== dirname( $plugin_folder ) ) {
6029 $plugin_folder = dirname( $plugin_folder );
6030 }
6031
6032 $this->_logger->departure( 'Folder Name = ' . $plugin_folder );
6033
6034 return $plugin_folder;
6035 }
6036
6037 #endregion ------------------------------------------------------------------
6038
6039 /* Account
6040 ------------------------------------------------------------------------------------------------------------------*/
6041
6042 /**
6043 * Find plugin's slug by plugin's basename.
6044 *
6045 * @author Vova Feldman (@svovaf)
6046 * @since 1.0.9
6047 *
6048 * @param string $plugin_base_name
6049 *
6050 * @return false|string
6051 */
6052 private static function find_slug_by_basename( $plugin_base_name ) {
6053 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
6054
6055 if ( ! array( $file_slug_map ) || ! isset( $file_slug_map[ $plugin_base_name ] ) ) {
6056 return false;
6057 }
6058
6059 return $file_slug_map[ $plugin_base_name ];
6060 }
6061
6062 /**
6063 * Store the map between the plugin's basename to the slug.
6064 *
6065 * @author Vova Feldman (@svovaf)
6066 * @since 1.0.9
6067 */
6068 private function store_file_slug_map() {
6069 $file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
6070
6071 if ( ! array( $file_slug_map ) ) {
6072 $file_slug_map = array();
6073 }
6074
6075 if ( ! isset( $file_slug_map[ $this->_plugin_basename ] ) ||
6076 $file_slug_map[ $this->_plugin_basename ] !== $this->_slug
6077 ) {
6078 $file_slug_map[ $this->_plugin_basename ] = $this->_slug;
6079 self::$_accounts->set_option( 'file_slug_map', $file_slug_map, true );
6080 }
6081 }
6082
6083 /**
6084 * @return FS_User[]
6085 */
6086 static function get_all_users() {
6087 $users = self::$_accounts->get_option( 'users', array() );
6088
6089 if ( ! is_array( $users ) ) {
6090 $users = array();
6091 }
6092
6093 return $users;
6094 }
6095
6096 /**
6097 * @param string $module_type
6098 *
6099 * @return FS_Site[]
6100 */
6101 private static function get_all_sites( $module_type = WP_FS__MODULE_TYPE_PLUGIN ) {
6102 $sites = self::get_account_option( 'sites', $module_type );
6103
6104 if ( ! is_array( $sites ) ) {
6105 $sites = array();
6106 }
6107
6108 return $sites;
6109 }
6110
6111 /**
6112 * @author Leo Fajardo (@leorw)
6113 *
6114 * @since 1.2.2
6115 *
6116 * @param string $option_name
6117 * @param string $module_type
6118 *
6119 * @return mixed
6120 */
6121 private static function get_account_option( $option_name, $module_type ) {
6122 if ( WP_FS__MODULE_TYPE_PLUGIN !== $module_type ) {
6123 $option_name = $module_type . '_' . $option_name;
6124 }
6125
6126 return self::$_accounts->get_option( $option_name, array() );
6127 }
6128
6129 /**
6130 * @author Leo Fajardo (@leorw)
6131 *
6132 * @since 1.2.2
6133 *
6134 * @param string $option_name
6135 * @param mixed $option_value
6136 * @param bool $store
6137 */
6138 private function set_account_option( $option_name, $option_value, $store ) {
6139 self::set_account_option_by_module(
6140 $this->_module_type,
6141 $option_name,
6142 $option_value,
6143 $store
6144 );
6145 }
6146
6147 /**
6148 * @author Vova Feldman (@svovaf)
6149 *
6150 * @since 1.2.2.7
6151 *
6152 * @param string $module_type
6153 * @param string $option_name
6154 * @param mixed $option_value
6155 * @param bool $store
6156 */
6157 private static function set_account_option_by_module( $module_type, $option_name, $option_value, $store ) {
6158 if ( WP_FS__MODULE_TYPE_PLUGIN != $module_type ) {
6159 $option_name = $module_type . '_' . $option_name;
6160 }
6161
6162 self::$_accounts->set_option( $option_name, $option_value, $store );
6163 }
6164
6165 /**
6166 * @author Vova Feldman (@svovaf)
6167 * @since 1.0.6
6168 *
6169 * @param string $module_type
6170 *
6171 * @return FS_Plugin_License[]
6172 */
6173 private static function get_all_licenses( $module_type = WP_FS__MODULE_TYPE_PLUGIN ) {
6174 $licenses = self::get_account_option( 'licenses', $module_type );
6175
6176 if ( ! is_array( $licenses ) ) {
6177 $licenses = array();
6178 }
6179
6180 return $licenses;
6181 }
6182
6183 /**
6184 * @param string|bool $module_type
6185 *
6186 * @return FS_Plugin_Plan[]
6187 */
6188 private static function get_all_plans( $module_type = false ) {
6189 $plans = self::get_account_option( 'plans', $module_type );
6190
6191 if ( ! is_array( $plans ) ) {
6192 $plans = array();
6193 }
6194
6195 return $plans;
6196 }
6197
6198 /**
6199 * @author Vova Feldman (@svovaf)
6200 * @since 1.0.4
6201 *
6202 * @return FS_Plugin_Tag[]
6203 */
6204 private static function get_all_updates() {
6205 $updates = self::$_accounts->get_option( 'updates', array() );
6206
6207 if ( ! is_array( $updates ) ) {
6208 $updates = array();
6209 }
6210
6211 return $updates;
6212 }
6213
6214 /**
6215 * @author Vova Feldman (@svovaf)
6216 * @since 1.0.6
6217 *
6218 * @return array<number,FS_Plugin[]>|false
6219 */
6220 private static function get_all_addons() {
6221 $addons = self::$_accounts->get_option( 'addons', array() );
6222
6223 if ( ! is_array( $addons ) ) {
6224 $addons = array();
6225 }
6226
6227 return $addons;
6228 }
6229
6230 /**
6231 * @author Vova Feldman (@svovaf)
6232 * @since 1.0.6
6233 *
6234 * @return FS_Plugin[]|false
6235 */
6236 private static function get_all_account_addons() {
6237 $addons = self::$_accounts->get_option( 'account_addons', array() );
6238
6239 if ( ! is_array( $addons ) ) {
6240 $addons = array();
6241 }
6242
6243 return $addons;
6244 }
6245
6246 /**
6247 * Check if user has connected his account (opted-in).
6248 *
6249 * Note:
6250 * If the user opted-in and opted-out on a later stage,
6251 * this will still return true. If you want to check if the
6252 * user is currently opted-in, use:
6253 * `$fs->is_registered() && $fs->is_tracking_allowed()`
6254 *
6255 * @author Vova Feldman (@svovaf)
6256 * @since 1.0.1
6257 * @return bool
6258 */
6259 function is_registered() {
6260 return is_object( $this->_user );
6261 }
6262
6263 /**
6264 * Returns TRUE if the user opted-in and didn't disconnect (opt-out).
6265 *
6266 * @author Leo Fajardo (@leorw)
6267 * @since 1.2.1.5
6268 *
6269 * @return bool
6270 */
6271 function is_tracking_allowed() {
6272 return ( is_object( $this->_site ) && true !== $this->_site->is_disconnected );
6273 }
6274
6275 /**
6276 * @author Vova Feldman (@svovaf)
6277 * @since 1.0.4
6278 *
6279 * @return FS_Plugin
6280 */
6281 function get_plugin() {
6282 return $this->_plugin;
6283 }
6284
6285 /**
6286 * @author Vova Feldman (@svovaf)
6287 * @since 1.0.3
6288 *
6289 * @return FS_User
6290 */
6291 function get_user() {
6292 return $this->_user;
6293 }
6294
6295 /**
6296 * @author Vova Feldman (@svovaf)
6297 * @since 1.0.3
6298 *
6299 * @return FS_Site
6300 */
6301 function get_site() {
6302 return $this->_site;
6303 }
6304
6305 /**
6306 * Get plugin add-ons.
6307 *
6308 * @author Vova Feldman (@svovaf)
6309 * @since 1.0.6
6310 *
6311 * @since 1.1.7.3 If not yet loaded, fetch data from the API.
6312 *
6313 * @param bool $flush
6314 *
6315 * @return FS_Plugin[]|false
6316 */
6317 function get_addons( $flush = false ) {
6318 $this->_logger->entrance();
6319
6320 if ( ! $this->_has_addons ) {
6321 return false;
6322 }
6323
6324 $addons = $this->sync_addons( $flush );
6325
6326 return ( ! is_array( $addons ) || empty( $addons ) ) ?
6327 false :
6328 $addons;
6329 }
6330
6331 /**
6332 * @author Vova Feldman (@svovaf)
6333 * @since 1.0.6
6334 *
6335 * @return FS_Plugin[]|false
6336 */
6337 function get_account_addons() {
6338 $this->_logger->entrance();
6339
6340 $addons = self::get_all_account_addons();
6341
6342 if ( ! is_array( $addons ) ||
6343 ! isset( $addons[ $this->_plugin->id ] ) ||
6344 ! is_array( $addons[ $this->_plugin->id ] ) ||
6345 0 === count( $addons[ $this->_plugin->id ] )
6346 ) {
6347 return false;
6348 }
6349
6350 return $addons[ $this->_plugin->id ];
6351 }
6352
6353 /**
6354 * Check if user has any
6355 *
6356 * @author Vova Feldman (@svovaf)
6357 * @since 1.1.6
6358 *
6359 * @return bool
6360 */
6361 function has_account_addons() {
6362 $addons = $this->get_account_addons();
6363
6364 return is_array( $addons ) && ( 0 < count( $addons ) );
6365 }
6366
6367
6368 /**
6369 * Get add-on by ID (from local data).
6370 *
6371 * @author Vova Feldman (@svovaf)
6372 * @since 1.0.6
6373 *
6374 * @param number $id
6375 *
6376 * @return FS_Plugin|false
6377 */
6378 function get_addon( $id ) {
6379 $this->_logger->entrance();
6380
6381 $addons = $this->get_addons();
6382
6383 if ( is_array( $addons ) ) {
6384 foreach ( $addons as $addon ) {
6385 if ( $id == $addon->id ) {
6386 return $addon;
6387 }
6388 }
6389 }
6390
6391 return false;
6392 }
6393
6394 /**
6395 * Get add-on by slug (from local data).
6396 *
6397 * @author Vova Feldman (@svovaf)
6398 * @since 1.0.6
6399 *
6400 * @param string $slug
6401 *
6402 * @param bool $flush
6403 *
6404 * @return FS_Plugin|false
6405 */
6406 function get_addon_by_slug( $slug, $flush = false ) {
6407 $this->_logger->entrance();
6408
6409 $addons = $this->get_addons( $flush );
6410
6411 if ( is_array( $addons ) ) {
6412 foreach ( $addons as $addon ) {
6413 if ( $slug === $addon->slug ) {
6414 return $addon;
6415 }
6416 }
6417 }
6418
6419 return false;
6420 }
6421
6422 #----------------------------------------------------------------------------------
6423 #region Plans & Licensing
6424 #----------------------------------------------------------------------------------
6425
6426 /**
6427 * Check if running premium plugin code.
6428 *
6429 * @author Vova Feldman (@svovaf)
6430 * @since 1.0.5
6431 *
6432 * @return bool
6433 */
6434 function is_premium() {
6435 return $this->_plugin->is_premium;
6436 }
6437
6438 /**
6439 * Get site's plan ID.
6440 *
6441 * @author Vova Feldman (@svovaf)
6442 * @since 1.0.2
6443 *
6444 * @return number
6445 */
6446 function get_plan_id() {
6447 return $this->_site->plan->id;
6448 }
6449
6450 /**
6451 * Get site's plan title.
6452 *
6453 * @author Vova Feldman (@svovaf)
6454 * @since 1.0.2
6455 *
6456 * @return string
6457 */
6458 function get_plan_title() {
6459 return $this->_site->plan->title;
6460 }
6461
6462 /**
6463 * @author Vova Feldman (@svovaf)
6464 * @since 1.0.9
6465 *
6466 * @return FS_Plugin_Plan|false
6467 */
6468 function get_plan() {
6469 return is_object( $this->_site->plan ) ?
6470 $this->_site->plan :
6471 false;
6472 }
6473
6474 /**
6475 * @author Vova Feldman (@svovaf)
6476 * @since 1.0.3
6477 *
6478 * @return bool
6479 */
6480 function is_trial() {
6481 $this->_logger->entrance();
6482
6483 if ( ! $this->is_registered() ) {
6484 return false;
6485 }
6486
6487 return $this->_site->is_trial();
6488 }
6489
6490 /**
6491 * Check if currently in a trial with payment method (credit card or paypal).
6492 *
6493 * @author Vova Feldman (@svovaf)
6494 * @since 1.1.7
6495 *
6496 * @return bool
6497 */
6498 function is_paid_trial() {
6499 $this->_logger->entrance();
6500
6501 if ( ! $this->is_trial() ) {
6502 return false;
6503 }
6504
6505 return $this->has_active_valid_license() && ( $this->_site->trial_plan_id == $this->_license->plan_id );
6506 }
6507
6508 /**
6509 * Check if trial already utilized.
6510 *
6511 * @since 1.0.9
6512 *
6513 * @return bool
6514 */
6515 function is_trial_utilized() {
6516 $this->_logger->entrance();
6517
6518 if ( ! $this->is_registered() ) {
6519 return false;
6520 }
6521
6522 return $this->_site->is_trial_utilized();
6523 }
6524
6525 /**
6526 * Get trial plan information (if in trial).
6527 *
6528 * @author Vova Feldman (@svovaf)
6529 * @since 1.0.9
6530 *
6531 * @return bool|FS_Plugin_Plan
6532 */
6533 function get_trial_plan() {
6534 $this->_logger->entrance();
6535
6536 if ( ! $this->is_trial() ) {
6537 return false;
6538 }
6539
6540 return $this->_storage->trial_plan;
6541 }
6542
6543 /**
6544 * Check if the user has an activate, non-expired license on current plugin's install.
6545 *
6546 * @since 1.0.9
6547 *
6548 * @return bool
6549 */
6550 function is_paying() {
6551 $this->_logger->entrance();
6552
6553 if ( ! $this->is_registered() ) {
6554 return false;
6555 }
6556
6557 if ( ! $this->has_paid_plan() ) {
6558 return false;
6559 }
6560
6561 return (
6562 ! $this->is_trial() &&
6563 'free' !== $this->_site->plan->name &&
6564 $this->has_active_valid_license()
6565 );
6566 }
6567
6568 /**
6569 * @author Vova Feldman (@svovaf)
6570 * @since 1.0.4
6571 *
6572 * @return bool
6573 */
6574 function is_free_plan() {
6575 if ( ! $this->is_registered() ) {
6576 return true;
6577 }
6578
6579 if ( ! $this->has_paid_plan() ) {
6580 return true;
6581 }
6582
6583 return (
6584 'free' === $this->_site->plan->name ||
6585 ! $this->has_features_enabled_license()
6586 );
6587 }
6588
6589 /**
6590 * @author Vova Feldman (@svovaf)
6591 * @since 1.0.5
6592 *
6593 * @return bool
6594 */
6595 function _has_premium_license() {
6596 $this->_logger->entrance();
6597
6598 $premium_license = $this->_get_available_premium_license();
6599
6600 return ( false !== $premium_license );
6601 }
6602
6603 /**
6604 * Check if user has any licenses associated with the plugin (including expired or blocking).
6605 *
6606 * @author Vova Feldman (@svovaf)
6607 * @since 1.1.7.3
6608 *
6609 * @return bool
6610 */
6611 private function has_any_license() {
6612 return is_array( $this->_licenses ) && ( 0 < count( $this->_licenses ) );
6613 }
6614
6615 /**
6616 * @author Vova Feldman (@svovaf)
6617 * @since 1.0.5
6618 *
6619 * @return FS_Plugin_License|false
6620 */
6621 function _get_available_premium_license() {
6622 $this->_logger->entrance();
6623
6624 if ( ! $this->has_paid_plan() ) {
6625 return false;
6626 }
6627
6628 if ( is_array( $this->_licenses ) ) {
6629 foreach ( $this->_licenses as $license ) {
6630 if ( ! $license->is_utilized() && $license->is_features_enabled() ) {
6631 return $license;
6632 }
6633 }
6634 }
6635
6636 return false;
6637 }
6638
6639 /**
6640 * Sync local plugin plans with remote server.
6641 *
6642 * @author Vova Feldman (@svovaf)
6643 * @since 1.0.5
6644 *
6645 * @return FS_Plugin_Plan[]|object
6646 */
6647 function _sync_plans() {
6648 $plans = $this->_fetch_plugin_plans();
6649
6650 if ( $this->is_array_instanceof( $plans, 'FS_Plugin_Plan' ) ) {
6651 $this->_plans = $plans;
6652 $this->_store_plans();
6653 }
6654
6655 $this->do_action( 'after_plans_sync', $plans );
6656
6657 return $this->_plans;
6658 }
6659
6660 /**
6661 * @author Vova Feldman (@svovaf)
6662 * @since 1.0.5
6663 *
6664 * @param number $id
6665 *
6666 * @return FS_Plugin_Plan|false
6667 */
6668 function _get_plan_by_id( $id ) {
6669 $this->_logger->entrance();
6670
6671 if ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) {
6672 $this->_sync_plans();
6673 }
6674
6675 foreach ( $this->_plans as $plan ) {
6676 if ( $id == $plan->id ) {
6677 return $plan;
6678 }
6679 }
6680
6681 return false;
6682 }
6683
6684 /**
6685 * @author Vova Feldman (@svovaf)
6686 * @since 1.1.8.1
6687 *
6688 * @param string $name
6689 *
6690 * @return FS_Plugin_Plan|false
6691 */
6692 private function get_plan_by_name( $name ) {
6693 $this->_logger->entrance();
6694
6695 if ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) {
6696 $this->_sync_plans();
6697 }
6698
6699 foreach ( $this->_plans as $plan ) {
6700 if ( $name == $plan->name ) {
6701 return $plan;
6702 }
6703 }
6704
6705 return false;
6706 }
6707
6708 /**
6709 * Sync local plugin plans with remote server.
6710 *
6711 * @author Vova Feldman (@svovaf)
6712 * @since 1.0.6
6713 *
6714 * @param number|bool $site_license_id
6715 *
6716 * @return FS_Plugin_License[]|object
6717 */
6718 function _sync_licenses( $site_license_id = false ) {
6719 $licenses = $this->_fetch_licenses( false, $site_license_id );
6720
6721 if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) {
6722 $this->_licenses = $licenses;
6723 $this->_store_licenses();
6724 }
6725
6726 // Update current license.
6727 if ( is_object( $this->_license ) ) {
6728 $this->_license = $this->_get_license_by_id( $this->_license->id );
6729 }
6730
6731 return $this->_licenses;
6732 }
6733
6734 /**
6735 * @author Vova Feldman (@svovaf)
6736 * @since 1.0.5
6737 *
6738 * @param number $id
6739 *
6740 * @return FS_Plugin_License|false
6741 */
6742 function _get_license_by_id( $id ) {
6743 $this->_logger->entrance();
6744
6745 if ( ! is_numeric( $id ) ) {
6746 return false;
6747 }
6748
6749 if ( ! $this->has_any_license() ) {
6750 $this->_sync_licenses();
6751 }
6752
6753 foreach ( $this->_licenses as $license ) {
6754 if ( $id == $license->id ) {
6755 return $license;
6756 }
6757 }
6758
6759 return false;
6760 }
6761
6762 /**
6763 * Sync site's license with user licenses.
6764 *
6765 * @author Vova Feldman (@svovaf)
6766 * @since 1.0.6
6767 *
6768 * @param FS_Plugin_License|null $new_license
6769 */
6770 function _update_site_license( $new_license ) {
6771 $this->_logger->entrance();
6772
6773 $this->_license = $new_license;
6774
6775 if ( ! is_object( $new_license ) ) {
6776 $this->_site->license_id = null;
6777 $this->_sync_site_subscription( null );
6778
6779 return;
6780 }
6781
6782 $this->_site->license_id = $this->_license->id;
6783
6784 if ( ! is_array( $this->_licenses ) ) {
6785 $this->_licenses = array();
6786 }
6787
6788 $is_license_found = false;
6789 for ( $i = 0, $len = count( $this->_licenses ); $i < $len; $i ++ ) {
6790 if ( $new_license->id == $this->_licenses[ $i ]->id ) {
6791 $this->_licenses[ $i ] = $new_license;
6792
6793 $is_license_found = true;
6794 break;
6795 }
6796 }
6797
6798 // If new license just append.
6799 if ( ! $is_license_found ) {
6800 $this->_licenses[] = $new_license;
6801 }
6802
6803 $this->_sync_site_subscription( $new_license );
6804 }
6805
6806 /**
6807 * Sync site's subscription.
6808 *
6809 * @author Vova Feldman (@svovaf)
6810 * @since 1.0.9
6811 *
6812 * @param FS_Plugin_License|null $license
6813 *
6814 * @return bool|\FS_Subscription
6815 */
6816 private function _sync_site_subscription( $license ) {
6817 if ( ! is_object( $license ) ) {
6818 unset( $this->_storage->subscription );
6819
6820 return false;
6821 }
6822
6823 // Load subscription details if not lifetime.
6824 $subscription = $license->is_lifetime() ?
6825 false :
6826 $this->_fetch_site_license_subscription();
6827
6828 if ( is_object( $subscription ) && ! isset( $subscription->error ) ) {
6829 $this->_storage->subscription = $subscription;
6830 } else {
6831 unset( $this->_storage->subscription );
6832 }
6833
6834 return $subscription;
6835 }
6836
6837 /**
6838 * @author Vova Feldman (@svovaf)
6839 * @since 1.0.6
6840 *
6841 * @return bool|\FS_Plugin_License
6842 */
6843 function _get_license() {
6844 return $this->_license;
6845 }
6846
6847 /**
6848 * @return bool|\FS_Subscription
6849 */
6850 function _get_subscription() {
6851 return isset( $this->_storage->subscription ) ?
6852 $this->_storage->subscription :
6853 false;
6854 }
6855
6856 /**
6857 * @author Vova Feldman (@svovaf)
6858 * @since 1.0.2
6859 *
6860 * @param string $plan Plan name
6861 * @param bool $exact If true, looks for exact plan. If false, also check "higher" plans.
6862 *
6863 * @return bool
6864 */
6865 function is_plan( $plan, $exact = false ) {
6866 $this->_logger->entrance();
6867
6868 if ( ! $this->is_registered() ) {
6869 return false;
6870 }
6871
6872 $plan = strtolower( $plan );
6873
6874 if ( $this->_site->plan->name === $plan ) // Exact plan.
6875 {
6876 return true;
6877 } else if ( $exact ) // Required exact, but plans are different.
6878 {
6879 return false;
6880 }
6881
6882 $current_plan_order = - 1;
6883 $required_plan_order = - 1;
6884 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
6885 if ( $plan === $this->_plans[ $i ]->name ) {
6886 $required_plan_order = $i;
6887 } else if ( $this->_site->plan->name === $this->_plans[ $i ]->name ) {
6888 $current_plan_order = $i;
6889 }
6890 }
6891
6892 return ( $current_plan_order > $required_plan_order );
6893 }
6894
6895 /**
6896 * Check if module has only one plan.
6897 *
6898 * @author Vova Feldman (@svovaf)
6899 * @since 1.2.1.7
6900 *
6901 * @return bool
6902 */
6903 function is_single_plan() {
6904 $this->_logger->entrance();
6905
6906 if ( ! $this->is_registered() ||
6907 ! is_array( $this->_plans ) ||
6908 0 === count( $this->_plans )
6909 ) {
6910 return true;
6911 }
6912
6913 return ( 1 === count( $this->_plans ) );
6914 }
6915
6916 /**
6917 * Check if plan based on trial. If not in trial mode, should return false.
6918 *
6919 * @since 1.0.9
6920 *
6921 * @param string $plan Plan name
6922 * @param bool $exact If true, looks for exact plan. If false, also check "higher" plans.
6923 *
6924 * @return bool
6925 */
6926 function is_trial_plan( $plan, $exact = false ) {
6927 $this->_logger->entrance();
6928
6929 if ( ! $this->is_registered() ) {
6930 return false;
6931 }
6932
6933 if ( ! $this->is_trial() ) {
6934 return false;
6935 }
6936
6937 if ( ! isset( $this->_storage->trial_plan ) ) {
6938 // Store trial plan information.
6939 $this->_enrich_site_trial_plan( true );
6940 }
6941
6942 if ( $this->_storage->trial_plan->name === $plan ) // Exact plan.
6943 {
6944 return true;
6945 } else if ( $exact ) // Required exact, but plans are different.
6946 {
6947 return false;
6948 }
6949
6950 $current_plan_order = - 1;
6951 $required_plan_order = - 1;
6952 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
6953 if ( $plan === $this->_plans[ $i ]->name ) {
6954 $required_plan_order = $i;
6955 } else if ( $this->_storage->trial_plan->name === $this->_plans[ $i ]->name ) {
6956 $current_plan_order = $i;
6957 }
6958 }
6959
6960 return ( $current_plan_order > $required_plan_order );
6961 }
6962
6963 /**
6964 * Check if plugin has any paid plans.
6965 *
6966 * @author Vova Feldman (@svovaf)
6967 * @since 1.0.7
6968 *
6969 * @return bool
6970 */
6971 function has_paid_plan() {
6972 return $this->_has_paid_plans ||
6973 FS_Plan_Manager::instance()->has_paid_plan( $this->_plans );
6974 }
6975
6976 /**
6977 * Check if plugin has any plan with a trail.
6978 *
6979 * @author Vova Feldman (@svovaf)
6980 * @since 1.0.9
6981 *
6982 * @return bool
6983 */
6984 function has_trial_plan() {
6985 if ( ! $this->is_registered() ) {
6986 /**
6987 * @author Vova Feldman(@svovaf)
6988 * @since 1.2.1.5
6989 *
6990 * Allow setting a trial from the SDK without calling the API.
6991 * But, if the user did opt-in, continue using the real data from the API.
6992 */
6993 if ( $this->_trial_days >= 0 ) {
6994 return true;
6995 }
6996
6997 return false;
6998 }
6999
7000 return $this->_storage->get( 'has_trial_plan', false );
7001 }
7002
7003 /**
7004 * Check if plugin has any free plan, or is it premium only.
7005 *
7006 * Note: If no plans configured, assume plugin is free.
7007 *
7008 * @author Vova Feldman (@svovaf)
7009 * @since 1.0.7
7010 *
7011 * @return bool
7012 */
7013 function has_free_plan() {
7014 return ! $this->is_only_premium();
7015 }
7016
7017 /**
7018 * Displays a license activation dialog box when the user clicks on the "Activate License"
7019 * or "Change License" link on the plugins
7020 * page.
7021 *
7022 * @author Leo Fajardo (@leorw)
7023 * @since 1.1.9
7024 */
7025 function _add_license_activation_dialog_box() {
7026 $vars = array(
7027 'id' => $this->_module_id,
7028 );
7029
7030 fs_require_template( 'forms/license-activation.php', $vars );
7031 fs_require_template( 'forms/resend-key.php', $vars );
7032 }
7033
7034 /**
7035 * Displays the opt-out dialog box when the user clicks on the "Opt Out" link on the "Plugins"
7036 * page.
7037 *
7038 * @author Leo Fajardo (@leorw)
7039 * @since 1.2.1.5
7040 */
7041 function _add_optout_dialog() {
7042 if ( $this->is_theme() ) {
7043 $vars = null;
7044 fs_require_once_template( '/js/jquery.content-change.php', $vars );
7045 }
7046
7047 $vars = array( 'id' => $this->_module_id );
7048 fs_require_template( 'forms/optout.php', $vars );
7049 }
7050
7051 /**
7052 * Prepare page to include all required UI and logic for the license activation dialog.
7053 *
7054 * @author Vova Feldman (@svovaf)
7055 * @since 1.2.0
7056 */
7057 function _add_license_activation() {
7058 if ( ! $this->is_user_admin() ) {
7059 // Only admins can activate a license.
7060 return;
7061 }
7062
7063 if ( ! $this->has_paid_plan() ) {
7064 // Module doesn't have any paid plans.
7065 return;
7066 }
7067
7068 if ( ! $this->is_premium() ) {
7069 // Only add license activation logic to the premium version.
7070 return;
7071 }
7072
7073 // Add license activation link and AJAX request handler.
7074 if ( self::is_plugins_page() ) {
7075 /**
7076 * @since 1.2.0 Add license action link only on plugins page.
7077 */
7078 $this->_add_license_action_link();
7079 }
7080
7081 // Add license activation AJAX callback.
7082 $this->add_ajax_action( 'activate_license', array( &$this, '_activate_license_ajax_action' ) );
7083
7084 // Add resend license AJAX callback.
7085 $this->add_ajax_action( 'resend_license_key', array( &$this, '_resend_license_key_ajax_action' ) );
7086 }
7087
7088 /**
7089 * @author Leo Fajardo (@leorw)
7090 * @since 1.1.9
7091 */
7092 function _activate_license_ajax_action() {
7093 $this->_logger->entrance();
7094
7095 $this->check_ajax_referer( 'activate_license' );
7096
7097 $license_key = trim( fs_request_get( 'license_key' ) );
7098
7099 if ( empty( $license_key ) ) {
7100 exit;
7101 }
7102
7103 $plugin_id = fs_request_get( 'module_id', '', 'post' );
7104 $fs = ( $plugin_id == $this->_module_id ) ?
7105 $this :
7106 $this->get_addon_instance( $plugin_id );
7107
7108 $error = false;
7109 $next_page = false;
7110
7111 if ( $fs->is_registered() ) {
7112 $api = $fs->get_api_site_scope();
7113 $install = $api->call( '/', 'put', array(
7114 'license_key' => $fs->apply_filters( 'license_key', $license_key )
7115 ) );
7116
7117 if ( isset( $install->error ) ) {
7118 $error = $install->error->message;
7119 } else {
7120 $fs->_sync_license( true );
7121
7122 $next_page = $fs->is_addon() ?
7123 $fs->get_parent_instance()->get_account_url() :
7124 $fs->get_account_url();
7125
7126 $fs->reconnect_locally();
7127 }
7128 } else {
7129 $next_page = $fs->opt_in( false, false, false, $license_key );
7130
7131 if ( isset( $next_page->error ) ) {
7132 $error = $next_page->error;
7133 }
7134 }
7135
7136 $result = array(
7137 'success' => ( false === $error )
7138 );
7139
7140 if ( false !== $error ) {
7141 $result['error'] = $error;
7142 } else {
7143 $result['next_page'] = $next_page;
7144 }
7145
7146 echo json_encode( $result );
7147
7148 exit;
7149 }
7150
7151 /**
7152 * Billing update AJAX callback.
7153 *
7154 * @author Vova Feldman (@svovaf)
7155 * @since 1.2.1.5
7156 */
7157 function _update_billing_ajax_action() {
7158 $this->_logger->entrance();
7159
7160 $this->check_ajax_referer( 'update_billing' );
7161
7162 if ( ! $this->is_user_admin() ) {
7163 // Only for admins.
7164 self::shoot_ajax_failure();
7165 }
7166
7167 $billing = fs_request_get( 'billing' );
7168
7169 $api = $this->get_api_user_scope();
7170 $result = $api->call( '/billing.json', 'put', array_merge( $billing, array(
7171 'plugin_id' => $this->get_parent_id(),
7172 ) ) );
7173
7174 if ( ! $this->is_api_result_entity( $result ) ) {
7175 self::shoot_ajax_failure();
7176 }
7177
7178 // Purge cached billing.
7179 $this->get_api_user_scope()->purge_cache( 'billing.json' );
7180
7181 self::shoot_ajax_success();
7182 }
7183
7184 /**
7185 * Trial start for anonymous users (AJAX callback).
7186 *
7187 * @author Vova Feldman (@svovaf)
7188 * @since 1.2.1.5
7189 */
7190 function _start_trial_ajax_action() {
7191 $this->_logger->entrance();
7192
7193 $this->check_ajax_referer( 'start_trial' );
7194
7195 if ( ! $this->is_user_admin() ) {
7196 // Only for admins.
7197 self::shoot_ajax_failure();
7198 }
7199
7200 $trial_data = fs_request_get( 'trial' );
7201
7202 $next_page = $this->opt_in(
7203 false,
7204 false,
7205 false,
7206 false,
7207 false,
7208 $trial_data['plan_id']
7209 );
7210
7211 if ( is_object( $next_page ) && $this->is_api_error( $next_page ) ) {
7212 self::shoot_ajax_failure(
7213 isset( $next_page->error ) ?
7214 $next_page->error->message :
7215 var_export( $next_page, true )
7216 );
7217 }
7218
7219 $this->shoot_ajax_success( array(
7220 'next_page' => $next_page,
7221 ) );
7222 }
7223
7224 /**
7225 * @author Leo Fajardo (@leorw)
7226 * @since 1.2.0
7227 */
7228 function _resend_license_key_ajax_action() {
7229 $this->_logger->entrance();
7230
7231 $this->check_ajax_referer( 'resend_license_key' );
7232
7233 $email_address = sanitize_email( trim( fs_request_get( 'email', '', 'post' ) ) );
7234
7235 if ( empty( $email_address ) ) {
7236 exit;
7237 }
7238
7239 $error = false;
7240
7241 $api = $this->get_api_plugin_scope();
7242 $result = $api->call( '/licenses/resend.json', 'post',
7243 array(
7244 'email' => $email_address,
7245 'url' => home_url(),
7246 )
7247 );
7248
7249 if ( is_object( $result ) && isset( $result->error ) ) {
7250 $error = $result->error;
7251
7252 if ( in_array( $error->code, array( 'invalid_email', 'no_user' ) ) ) {
7253 $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' );
7254 } else if ( 'no_license' === $error->code ) {
7255 $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' );
7256 } else {
7257 $error = $error->message;
7258 }
7259 }
7260
7261 $licenses = array(
7262 'success' => ( false === $error )
7263 );
7264
7265 if ( false !== $error ) {
7266 $licenses['error'] = sprintf( '%s... %s', $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ), strtolower( $error ) );
7267 }
7268
7269 echo json_encode( $licenses );
7270
7271 exit;
7272 }
7273
7274 /**
7275 * @author Vova Feldman (@svovaf)
7276 * @since 1.2.1.8
7277 *
7278 * @var string
7279 */
7280 private static $_pagenow;
7281
7282 /**
7283 * Get current page or the referer if executing a WP AJAX request.
7284 *
7285 * @author Vova Feldman (@svovaf)
7286 * @since 1.2.1.8
7287 *
7288 * @return string
7289 */
7290 static function get_current_page() {
7291 if ( ! isset( self::$_pagenow ) ) {
7292 global $pagenow;
7293
7294 self::$_pagenow = $pagenow;
7295
7296 if ( self::is_ajax() &&
7297 'admin-ajax.php' === $pagenow
7298 ) {
7299 $referer = fs_get_raw_referer();
7300
7301 if ( is_string( $referer ) ) {
7302 $parts = explode( '?', $referer );
7303
7304 self::$_pagenow = basename( $parts[0] );
7305 }
7306 }
7307 }
7308
7309 return self::$_pagenow;
7310 }
7311
7312 /**
7313 * Helper method to check if user in the plugins page.
7314 *
7315 * @author Vova Feldman (@svovaf)
7316 * @since 1.2.1.5
7317 *
7318 * @return bool
7319 */
7320 static function is_plugins_page() {
7321 return ( 'plugins.php' === self::get_current_page() );
7322 }
7323
7324 /**
7325 * Helper method to check if user in the themes page.
7326 *
7327 * @author Vova Feldman (@svovaf)
7328 * @since 1.2.2.6
7329 *
7330 * @return bool
7331 */
7332 static function is_themes_page() {
7333 return ( 'themes.php' === self::get_current_page() );
7334 }
7335
7336 #----------------------------------------------------------------------------------
7337 #region Affiliation
7338 #----------------------------------------------------------------------------------
7339
7340 /**
7341 * @author Leo Fajardo
7342 * @since 1.2.3
7343 *
7344 * @return bool
7345 */
7346 function has_affiliate_program() {
7347 if ( ! is_object( $this->_plugin ) ) {
7348 return false;
7349 }
7350
7351 return $this->_plugin->has_affiliate_program();
7352 }
7353
7354 /**
7355 * @author Leo Fajardo (@leorw)
7356 * @since 1.2.4
7357 */
7358 private function fetch_affiliate_terms() {
7359 if ( ! is_object( $this->plugin_affiliate_terms ) ) {
7360 $plugins_api = $this->get_api_plugin_scope();
7361 $affiliate_terms = $plugins_api->get( '/aff.json?type=affiliation', false, WP_FS__TIME_WEEK_IN_SEC );
7362
7363 if ( ! $this->is_api_result_entity( $affiliate_terms ) ) {
7364 return;
7365 }
7366
7367 $this->plugin_affiliate_terms = new FS_AffiliateTerms( $affiliate_terms );
7368 }
7369 }
7370
7371 /**
7372 * @author Leo Fajardo (@leorw)
7373 * @since 1.2.4
7374 */
7375 private function fetch_affiliate_and_custom_terms() {
7376 if ( ! empty( $this->_storage->affiliate_application_data ) ) {
7377 $application_data = $this->_storage->affiliate_application_data;
7378 $flush = ( ! isset( $application_data['status'] ) || 'pending' === $application_data['status'] );
7379
7380 $users_api = $this->get_api_user_scope();
7381 $result = $users_api->get( "/plugins/{$this->_plugin->id}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json", $flush, WP_FS__TIME_WEEK_IN_SEC );
7382 if ( $this->is_api_result_object( $result, 'affiliates' ) ) {
7383 if ( ! empty( $result->affiliates ) ) {
7384 $affiliate = new FS_Affiliate( $result->affiliates[0] );
7385
7386 if ( ! isset( $application_data['status'] ) || $application_data['status'] !== $affiliate->status ) {
7387 $application_data['status'] = $affiliate->status;
7388 $this->_storage->affiliate_application_data = $application_data;
7389 }
7390
7391 if ( $affiliate->is_using_custom_terms ) {
7392 $affiliate_terms = $users_api->get( "/plugins/{$this->_plugin->id}/affiliates/{$affiliate->id}/aff/{$affiliate->custom_affiliate_terms_id}.json", $flush, WP_FS__TIME_WEEK_IN_SEC );
7393 if ( $this->is_api_result_entity( $affiliate_terms ) ) {
7394 $this->custom_affiliate_terms = new FS_AffiliateTerms( $affiliate_terms );
7395 }
7396 }
7397
7398 $this->affiliate = $affiliate;
7399 }
7400 }
7401 }
7402 }
7403
7404 /**
7405 * @author Leo Fajardo (@leorw)
7406 * @since 1.2.3
7407 */
7408 private function fetch_affiliate_and_terms() {
7409 $this->_logger->entrance();
7410
7411 $this->fetch_affiliate_terms();
7412 $this->fetch_affiliate_and_custom_terms();
7413 }
7414
7415 /**
7416 * @author Leo Fajardo
7417 * @since 1.2.3
7418 *
7419 * @return FS_Affiliate
7420 */
7421 function get_affiliate() {
7422 return $this->affiliate;
7423 }
7424
7425
7426 /**
7427 * @author Leo Fajardo
7428 * @since 1.2.3
7429 *
7430 * @return FS_AffiliateTerms
7431 */
7432 function get_affiliate_terms() {
7433 return is_object( $this->custom_affiliate_terms ) ?
7434 $this->custom_affiliate_terms :
7435 $this->plugin_affiliate_terms;
7436 }
7437
7438 /**
7439 * @author Leo Fajardo
7440 * @since 1.2.3
7441 *
7442 * @return FS_Affiliate|null
7443 */
7444 function _submit_affiliate_application() {
7445 $this->_logger->entrance();
7446
7447 $this->check_ajax_referer( 'submit_affiliate_application' );
7448
7449 if ( ! $this->is_user_admin() ) {
7450 // Only for admins.
7451 self::shoot_ajax_failure();
7452 }
7453
7454 $affiliate = fs_request_get( 'affiliate' );
7455
7456 if ( empty( $affiliate['promotion_methods'] ) ) {
7457 unset( $affiliate['promotion_methods'] );
7458 }
7459
7460 if ( ! empty( $affiliate['additional_domains'] ) ) {
7461 $affiliate['additional_domains'] = array_unique( $affiliate['additional_domains'] );
7462 }
7463
7464 if ( ! $this->is_registered() ) {
7465 // Opt in but don't track usage.
7466 $next_page = $this->opt_in(
7467 false,
7468 false,
7469 false,
7470 false,
7471 false,
7472 false,
7473 true
7474 );
7475
7476 if ( is_object( $next_page ) && $this->is_api_error( $next_page ) ) {
7477 self::shoot_ajax_failure(
7478 isset( $next_page->error ) ?
7479 $next_page->error->message :
7480 var_export( $next_page, true )
7481 );
7482 } else if ( $this->is_pending_activation() ) {
7483 self::shoot_ajax_failure( $this->get_text_inline( 'Account is pending activation.', 'account-is-pending-activation' ) );
7484 }
7485 }
7486
7487 $this->fetch_affiliate_terms();
7488
7489 $api = $this->get_api_user_scope();
7490 $result = $api->call(
7491 ( "/plugins/{$this->_plugin->id}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json" ),
7492 'post',
7493 $affiliate
7494 );
7495
7496 if ( $this->is_api_error( $result ) ) {
7497 self::shoot_ajax_failure(
7498 isset( $result->error ) ?
7499 $result->error->message :
7500 var_export( $result, true )
7501 );
7502 }
7503 else
7504 {
7505 if ( $this->_admin_notices->has_sticky( 'affiliate_program' ) ) {
7506 $this->_admin_notices->remove_sticky( 'affiliate_program' );
7507 }
7508
7509 $affiliate_application_data = array(
7510 'status' => 'pending',
7511 'stats_description' => $affiliate['stats_description'],
7512 'promotion_method_description' => $affiliate['promotion_method_description'],
7513 );
7514
7515 if ( ! empty( $affiliate['promotion_methods'] ) ) {
7516 $affiliate_application_data['promotion_methods'] = $affiliate['promotion_methods'];
7517 }
7518
7519 if ( ! empty( $affiliate['domain'] ) ) {
7520 $affiliate_application_data['domain'] = $affiliate['domain'];
7521 }
7522
7523 if ( ! empty( $affiliate['additional_domains'] ) ) {
7524 $affiliate_application_data['additional_domains'] = $affiliate['additional_domains'];
7525 }
7526
7527 $this->_storage->affiliate_application_data = $affiliate_application_data;
7528 }
7529
7530 // Purge cached affiliate.
7531 $api->purge_cache( 'affiliate.json' );
7532
7533 self::shoot_ajax_success( $result );
7534 }
7535
7536 /**
7537 * @author Leo Fajardo
7538 * @since 1.2.3
7539 *
7540 * @return array|null
7541 */
7542 function get_affiliate_application_data() {
7543 if ( empty( $this->_storage->affiliate_application_data ) ) {
7544 return null;
7545 }
7546
7547 return $this->_storage->affiliate_application_data;
7548 }
7549
7550 #endregion Affiliation ------------------------------------------------------------
7551
7552 #----------------------------------------------------------------------------------
7553 #region URL Generators
7554 #----------------------------------------------------------------------------------
7555
7556 /**
7557 * Alias to pricing_url().
7558 *
7559 * @author Vova Feldman (@svovaf)
7560 * @since 1.0.2
7561 *
7562 * @uses pricing_url()
7563 *
7564 * @param string $period Billing cycle
7565 * @param bool $is_trial
7566 *
7567 * @return string
7568 */
7569 function get_upgrade_url( $period = WP_FS__PERIOD_ANNUALLY, $is_trial = false ) {
7570 return $this->pricing_url( $period, $is_trial );
7571 }
7572
7573 /**
7574 * @author Vova Feldman (@svovaf)
7575 * @since 1.0.9
7576 *
7577 * @uses get_upgrade_url()
7578 *
7579 * @return string
7580 */
7581 function get_trial_url() {
7582 return $this->get_upgrade_url( WP_FS__PERIOD_ANNUALLY, true );
7583 }
7584
7585 /**
7586 * Plugin's pricing URL.
7587 *
7588 * @author Vova Feldman (@svovaf)
7589 * @since 1.0.4
7590 *
7591 * @param string $billing_cycle Billing cycle
7592 *
7593 * @param bool $is_trial
7594 *
7595 * @return string
7596 */
7597 function pricing_url( $billing_cycle = WP_FS__PERIOD_ANNUALLY, $is_trial = false ) {
7598 $this->_logger->entrance();
7599
7600 $params = array(
7601 'billing_cycle' => $billing_cycle
7602 );
7603
7604 if ( $is_trial ) {
7605 $params['trial'] = 'true';
7606 }
7607
7608 return $this->_get_admin_page_url( 'pricing', $params );
7609 }
7610
7611 /**
7612 * Checkout page URL.
7613 *
7614 * @author Vova Feldman (@svovaf)
7615 * @since 1.0.6
7616 *
7617 * @param string $billing_cycle Billing cycle
7618 * @param bool $is_trial
7619 * @param array $extra (optional) Extra parameters, override other query params.
7620 *
7621 * @return string
7622 */
7623 function checkout_url(
7624 $billing_cycle = WP_FS__PERIOD_ANNUALLY,
7625 $is_trial = false,
7626 $extra = array()
7627 ) {
7628 $this->_logger->entrance();
7629
7630 $params = array(
7631 'checkout' => 'true',
7632 'billing_cycle' => $billing_cycle,
7633 );
7634
7635 if ( $is_trial ) {
7636 $params['trial'] = 'true';
7637 }
7638
7639 /**
7640 * Params in extra override other params.
7641 */
7642 $params = array_merge( $params, $extra );
7643
7644 return $this->_get_admin_page_url( 'pricing', $params );
7645 }
7646
7647 /**
7648 * Add-on checkout URL.
7649 *
7650 * @author Vova Feldman (@svovaf)
7651 * @since 1.1.7
7652 *
7653 * @param number $addon_id
7654 * @param number $pricing_id
7655 * @param string $billing_cycle
7656 * @param bool $is_trial
7657 *
7658 * @return string
7659 */
7660 function addon_checkout_url(
7661 $addon_id,
7662 $pricing_id,
7663 $billing_cycle = WP_FS__PERIOD_ANNUALLY,
7664 $is_trial = false
7665 ) {
7666 return $this->checkout_url( $billing_cycle, $is_trial, array(
7667 'plugin_id' => $addon_id,
7668 'pricing_id' => $pricing_id,
7669 ) );
7670 }
7671
7672 #endregion
7673
7674 #endregion ------------------------------------------------------------------
7675
7676 /**
7677 * Check if plugin has any add-ons.
7678 *
7679 * @author Vova Feldman (@svovaf)
7680 * @since 1.0.5
7681 *
7682 * @since 1.1.7.3 Base logic only on the parameter provided by the developer in the init function.
7683 *
7684 * @return bool
7685 */
7686 function has_addons() {
7687 $this->_logger->entrance();
7688
7689 return $this->_has_addons;
7690 }
7691
7692 /**
7693 * Check if plugin can work in anonymous mode.
7694 *
7695 * @author Vova Feldman (@svovaf)
7696 * @since 1.0.9
7697 *
7698 * @return bool
7699 *
7700 * @deprecated Please use is_enable_anonymous() instead
7701 */
7702 function enable_anonymous() {
7703 return $this->_enable_anonymous;
7704 }
7705
7706 /**
7707 * Check if plugin can work in anonymous mode.
7708 *
7709 * @author Vova Feldman (@svovaf)
7710 * @since 1.1.9
7711 *
7712 * @return bool
7713 */
7714 function is_enable_anonymous() {
7715 return $this->_enable_anonymous;
7716 }
7717
7718 /**
7719 * Check if plugin is premium only (no free plans).
7720 *
7721 * @author Vova Feldman (@svovaf)
7722 * @since 1.1.9
7723 *
7724 * @return bool
7725 */
7726 function is_only_premium() {
7727 return $this->_is_premium_only;
7728 }
7729
7730 /**
7731 * Checks if the plugin's type is "plugin". The other type is "theme".
7732 *
7733 * @author Leo Fajardo (@leorw)
7734 * @since 1.2.2
7735 *
7736 * @return bool
7737 */
7738 function is_plugin() {
7739 return ( WP_FS__MODULE_TYPE_PLUGIN === $this->_module_type );
7740 }
7741
7742 /**
7743 * @author Leo Fajardo (@leorw)
7744 * @since 1.2.2
7745 *
7746 * @return string
7747 */
7748 function get_module_type() {
7749 if ( ! isset( $this->_module_type ) ) {
7750 $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
7751 $this->_module_type = $id_slug_type_path_map[ $this->_module_id ]['type'];
7752 }
7753
7754 return $this->_module_type;
7755 }
7756
7757 /**
7758 * @author Leo Fajardo (@leorw)
7759 * @since 1.2.2
7760 *
7761 * @return string
7762 */
7763 function get_plugin_main_file_path() {
7764 return $this->_plugin_main_file_path;
7765 }
7766
7767 /**
7768 * Check if module has a premium code version.
7769 *
7770 * Serviceware module might be freemium without any
7771 * premium code version, where the paid features
7772 * are all part of the service.
7773 *
7774 * @author Vova Feldman (@svovaf)
7775 * @since 1.2.1.6
7776 *
7777 * @return bool
7778 */
7779 function has_premium_version() {
7780 return $this->_has_premium_version;
7781 }
7782
7783 /**
7784 * Check if feature supported with current site's plan.
7785 *
7786 * @author Vova Feldman (@svovaf)
7787 * @since 1.0.1
7788 *
7789 * @todo IMPLEMENT
7790 *
7791 * @param number $feature_id
7792 *
7793 * @throws Exception
7794 */
7795 function is_feature_supported( $feature_id ) {
7796 throw new Exception( 'not implemented' );
7797 }
7798
7799 /**
7800 * @author Vova Feldman (@svovaf)
7801 * @since 1.0.1
7802 *
7803 * @return bool Is running in SSL/HTTPS
7804 */
7805 function is_ssl() {
7806 return WP_FS__IS_HTTPS;
7807 }
7808
7809 /**
7810 * @author Vova Feldman (@svovaf)
7811 * @since 1.0.9
7812 *
7813 * @return bool Is running in AJAX call.
7814 *
7815 * @link http://wordpress.stackexchange.com/questions/70676/how-to-check-if-i-am-in-admin-ajax
7816 */
7817 static function is_ajax() {
7818 return ( defined( 'DOING_AJAX' ) && DOING_AJAX );
7819 }
7820
7821 /**
7822 * Check if it's an AJAX call targeted for the current module.
7823 *
7824 * @author Vova Feldman (@svovaf)
7825 * @since 1.2.0
7826 *
7827 * @param array|string $actions Collection of AJAX actions.
7828 *
7829 * @return bool
7830 */
7831 function is_ajax_action( $actions ) {
7832 // Verify it's an ajax call.
7833 if ( ! self::is_ajax() ) {
7834 return false;
7835 }
7836
7837 // Verify the call is relevant for the plugin.
7838 if ( $this->_module_id != fs_request_get( 'module_id' ) ) {
7839 return false;
7840 }
7841
7842 // Verify it's one of the specified actions.
7843 if ( is_string( $actions ) ) {
7844 $actions = explode( ',', $actions );
7845 }
7846
7847 if ( is_array( $actions ) && 0 < count( $actions ) ) {
7848 $ajax_action = fs_request_get( 'action' );
7849
7850 foreach ( $actions as $action ) {
7851 if ( $ajax_action === $this->get_action_tag( $action ) ) {
7852 return true;
7853 }
7854 }
7855 }
7856
7857 return false;
7858 }
7859
7860 /**
7861 * Check if it's an AJAX call targeted for current request.
7862 *
7863 * @author Vova Feldman (@svovaf)
7864 * @since 1.2.0
7865 *
7866 * @param array|string $actions Collection of AJAX actions.
7867 * @param number|null $module_id
7868 *
7869 * @return bool
7870 */
7871 static function is_ajax_action_static( $actions, $module_id = null ) {
7872 // Verify it's an ajax call.
7873 if ( ! self::is_ajax() ) {
7874 return false;
7875 }
7876
7877
7878 if ( ! empty( $module_id ) ) {
7879 // Verify the call is relevant for the plugin.
7880 if ( $module_id != fs_request_get( 'module_id' ) ) {
7881 return false;
7882 }
7883 }
7884
7885 // Verify it's one of the specified actions.
7886 if ( is_string( $actions ) ) {
7887 $actions = explode( ',', $actions );
7888 }
7889
7890 if ( is_array( $actions ) && 0 < count( $actions ) ) {
7891 $ajax_action = fs_request_get( 'action' );
7892
7893 foreach ( $actions as $action ) {
7894 if ( $ajax_action === self::get_ajax_action_static( $action, $module_id ) ) {
7895 return true;
7896 }
7897 }
7898 }
7899
7900 return false;
7901 }
7902
7903 /**
7904 * @author Vova Feldman (@svovaf)
7905 * @since 1.1.7
7906 *
7907 * @return bool
7908 */
7909 static function is_cron() {
7910 return ( defined( 'DOING_CRON' ) && DOING_CRON );
7911 }
7912
7913 /**
7914 * Check if a real user is visiting the admin dashboard.
7915 *
7916 * @author Vova Feldman (@svovaf)
7917 * @since 1.1.7
7918 *
7919 * @return bool
7920 */
7921 function is_user_in_admin() {
7922 return is_admin() && ! self::is_ajax() && ! self::is_cron();
7923 }
7924
7925 /**
7926 * Check if a real user is in the customizer view.
7927 *
7928 * @author Vova Feldman (@svovaf)
7929 * @since 1.2.2.7
7930 *
7931 * @return bool
7932 */
7933 static function is_customizer() {
7934 return is_customize_preview();
7935 }
7936
7937 /**
7938 * Check if running in HTTPS and if site's plan matching the specified plan.
7939 *
7940 * @param string $plan
7941 * @param bool $exact
7942 *
7943 * @return bool
7944 */
7945 function is_ssl_and_plan( $plan, $exact = false ) {
7946 return ( $this->is_ssl() && $this->is_plan( $plan, $exact ) );
7947 }
7948
7949 /**
7950 * Construct plugin's settings page URL.
7951 *
7952 * @author Vova Feldman (@svovaf)
7953 * @since 1.0.4
7954 *
7955 * @param string $page
7956 * @param array $params
7957 *
7958 * @return string
7959 */
7960 function _get_admin_page_url( $page = '', $params = array() ) {
7961 if ( 0 < count( $params ) ) {
7962 foreach ( $params as $k => $v ) {
7963 $params[ $k ] = urlencode( $v );
7964 }
7965 }
7966
7967 $page_param = $this->_menu->get_slug( $page );
7968
7969 if ( empty( $page ) &&
7970 $this->is_theme() &&
7971 // Show the opt-in as an overlay for free wp.org themes or themes without any settings page.
7972 ( $this->is_free_wp_org_theme() || ! $this->has_settings_menu() ) ) {
7973 $params[ $this->get_unique_affix() . '_show_optin' ] = 'true';
7974
7975 return add_query_arg(
7976 $params,
7977 admin_url( 'themes.php' )
7978 );
7979 }
7980
7981 if ( ! $this->has_settings_menu() ) {
7982 if ( ! empty( $page ) ) {
7983 // Module doesn't have a setting page, but since the request is for
7984 // a specific Freemius page, use the admin.php path.
7985 return add_query_arg( array_merge( $params, array(
7986 'page' => $page_param,
7987 ) ), admin_url( 'admin.php' ) );
7988 } else {
7989 if ( $this->is_activation_mode() ) {
7990 /**
7991 * @author Vova Feldman
7992 * @since 1.2.1.6
7993 *
7994 * If plugin doesn't have a settings page, create one for the opt-in screen.
7995 */
7996 return add_query_arg( array_merge( $params, array(
7997 'page' => $this->_slug,
7998 ) ), admin_url( 'admin.php', 'admin' ) );
7999 } else {
8000 // Plugin without a settings page.
8001 return add_query_arg(
8002 $params,
8003 admin_url( 'plugins.php' )
8004 );
8005 }
8006 }
8007 }
8008
8009 // Module has a submenu settings page.
8010 if ( ! $this->_menu->is_top_level() ) {
8011 $parent_slug = $this->_menu->get_parent_slug();
8012 $menu_file = ( false !== strpos( $parent_slug, '.php' ) ) ?
8013 $parent_slug :
8014 'admin.php';
8015
8016 return add_query_arg( array_merge( $params, array(
8017 'page' => $page_param,
8018 ) ), admin_url( $menu_file, 'admin' ) );
8019 }
8020
8021 // Module has a top level CPT settings page.
8022 if ( $this->_menu->is_cpt() ) {
8023 if ( empty( $page ) && $this->is_activation_mode() ) {
8024 return add_query_arg( array_merge( $params, array(
8025 'page' => $page_param
8026 ) ), admin_url( 'admin.php', 'admin' ) );
8027 } else {
8028 if ( ! empty( $page ) ) {
8029 $params['page'] = $page_param;
8030 }
8031
8032 return add_query_arg(
8033 $params,
8034 admin_url( $this->_menu->get_raw_slug(), 'admin' )
8035 );
8036 }
8037 }
8038
8039 // Module has a custom top level settings page.
8040 return add_query_arg( array_merge( $params, array(
8041 'page' => $page_param,
8042 ) ), admin_url( 'admin.php', 'admin' ) );
8043 }
8044
8045 /**
8046 * Check if currently in a specified admin page.
8047 *
8048 * @author Vova Feldman (@svovaf)
8049 * @since 1.2.2.7
8050 *
8051 * @param string $page
8052 *
8053 * @return bool
8054 */
8055 function is_admin_page( $page ) {
8056 return ( $this->_menu->get_slug( $page ) === fs_request_get( 'page', '', 'get' ) );
8057 }
8058
8059 /**
8060 * Get module's main admin setting page URL.
8061 *
8062 * @author Vova Feldman (@svovaf)
8063 * @since 1.2.2.7
8064 *
8065 * @return string
8066 */
8067 function main_menu_url() {
8068 return $this->_menu->main_menu_url();
8069 }
8070
8071 /**
8072 * Check if currently on the theme's setting page or
8073 * on any of the Freemius added pages (via tabs).
8074 *
8075 * @author Vova Feldman (@svovaf)
8076 * @since 1.2.2.7
8077 *
8078 * @return bool
8079 */
8080 function is_theme_settings_page() {
8081 return fs_starts_with(
8082 fs_request_get( 'page', '', 'get' ),
8083 $this->_menu->get_slug()
8084 );
8085 }
8086
8087 /**
8088 * Plugin's account page + sync license URL.
8089 *
8090 * @author Vova Feldman (@svovaf)
8091 * @since 1.1.9.1
8092 *
8093 * @param bool|number $plugin_id
8094 * @param bool $add_action_nonce
8095 * @param array $params
8096 *
8097 * @return string
8098 */
8099 function _get_sync_license_url( $plugin_id = false, $add_action_nonce = true, $params = array() ) {
8100 if ( is_numeric( $plugin_id ) ) {
8101 $params['plugin_id'] = $plugin_id;
8102 }
8103
8104 return $this->get_account_url(
8105 $this->get_unique_affix() . '_sync_license',
8106 $params,
8107 $add_action_nonce
8108 );
8109 }
8110
8111 /**
8112 * Plugin's account URL.
8113 *
8114 * @author Vova Feldman (@svovaf)
8115 * @since 1.0.4
8116 *
8117 * @param bool|string $action
8118 * @param array $params
8119 *
8120 * @param bool $add_action_nonce
8121 *
8122 * @return string
8123 */
8124 function get_account_url( $action = false, $params = array(), $add_action_nonce = true ) {
8125 if ( is_string( $action ) ) {
8126 $params['fs_action'] = $action;
8127 }
8128
8129 self::require_pluggable_essentials();
8130
8131 return ( $add_action_nonce && is_string( $action ) ) ?
8132 fs_nonce_url( $this->_get_admin_page_url( 'account', $params ), $action ) :
8133 $this->_get_admin_page_url( 'account', $params );
8134 }
8135
8136 /**
8137 * @author Vova Feldman (@svovaf)
8138 * @since 1.2.0
8139 *
8140 * @param string $tab
8141 * @param bool $action
8142 * @param array $params
8143 * @param bool $add_action_nonce
8144 *
8145 * @return string
8146 *
8147 * @uses get_account_url()
8148 */
8149 function get_account_tab_url( $tab, $action = false, $params = array(), $add_action_nonce = true ) {
8150 $params['tab'] = $tab;
8151
8152 return $this->get_account_url( $action, $params, $add_action_nonce );
8153 }
8154
8155 /**
8156 * Plugin's account URL.
8157 *
8158 * @author Vova Feldman (@svovaf)
8159 * @since 1.0.4
8160 *
8161 * @param bool|string $topic
8162 * @param bool|string $message
8163 *
8164 * @return string
8165 */
8166 function contact_url( $topic = false, $message = false ) {
8167 $params = array();
8168 if ( is_string( $topic ) ) {
8169 $params['topic'] = $topic;
8170 }
8171 if ( is_string( $message ) ) {
8172 $params['message'] = $message;
8173 }
8174
8175 if ( $this->is_addon() ) {
8176 $params['addon_id'] = $this->get_id();
8177
8178 return $this->get_parent_instance()->_get_admin_page_url( 'contact', $params );
8179 } else {
8180 return $this->_get_admin_page_url( 'contact', $params );
8181 }
8182 }
8183
8184 /**
8185 * Add-on direct info URL.
8186 *
8187 * @author Vova Feldman (@svovaf)
8188 * @since 1.1.0
8189 *
8190 * @param string $slug
8191 *
8192 * @return string
8193 */
8194 function addon_url( $slug ) {
8195 return $this->_get_admin_page_url( 'addons', array(
8196 'slug' => $slug
8197 ) );
8198 }
8199
8200 /* Logger
8201 ------------------------------------------------------------------------------------------------------------------*/
8202 /**
8203 * @param string $id
8204 * @param bool $prefix_slug
8205 *
8206 * @return FS_Logger
8207 */
8208 function get_logger( $id = '', $prefix_slug = true ) {
8209 return FS_Logger::get_logger( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id );
8210 }
8211
8212 /**
8213 * @param $id
8214 * @param bool $load_options
8215 * @param bool $prefix_slug
8216 *
8217 * @return FS_Option_Manager
8218 */
8219 function get_options_manager( $id, $load_options = false, $prefix_slug = true ) {
8220 return FS_Option_Manager::get_manager( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id, $load_options );
8221 }
8222
8223 /* Security
8224 ------------------------------------------------------------------------------------------------------------------*/
8225 private static function _encrypt( $str ) {
8226 if ( is_null( $str ) ) {
8227 return null;
8228 }
8229
8230 /**
8231 * The encrypt/decrypt functions are used to protect
8232 * the user from messing up with some of the sensitive
8233 * data stored for the module as a JSON in the database.
8234 *
8235 * I used the same suggested hack by the theme review team.
8236 * For more details, look at the function `Base64UrlDecode()`
8237 * in `./sdk/FreemiusBase.php`.
8238 *
8239 * @todo Remove this hack once the base64 error is removed from the Theme Check.
8240 *
8241 * @author Vova Feldman (@svovaf)
8242 * @since 1.2.2
8243 */
8244 $fn = 'base64' . '_encode';
8245
8246 return $fn( $str );
8247 }
8248
8249 static function _decrypt( $str ) {
8250 if ( is_null( $str ) ) {
8251 return null;
8252 }
8253
8254 /**
8255 * The encrypt/decrypt functions are used to protect
8256 * the user from messing up with some of the sensitive
8257 * data stored for the module as a JSON in the database.
8258 *
8259 * I used the same suggested hack by the theme review team.
8260 * For more details, look at the function `Base64UrlDecode()`
8261 * in `./sdk/FreemiusBase.php`.
8262 *
8263 * @todo Remove this hack once the base64 error is removed from the Theme Check.
8264 *
8265 * @author Vova Feldman (@svovaf)
8266 * @since 1.2.2
8267 */
8268 $fn = 'base64' . '_decode';
8269
8270 return $fn( $str );
8271 }
8272
8273 /**
8274 * @author Vova Feldman (@svovaf)
8275 * @since 1.0.5
8276 *
8277 * @param FS_Entity $entity
8278 *
8279 * @return FS_Entity Return an encrypted clone entity.
8280 */
8281 private static function _encrypt_entity( FS_Entity $entity ) {
8282 $clone = clone $entity;
8283 $props = get_object_vars( $entity );
8284
8285 foreach ( $props as $key => $val ) {
8286 $clone->{$key} = self::_encrypt( $val );
8287 }
8288
8289 return $clone;
8290 }
8291
8292 /**
8293 * @author Vova Feldman (@svovaf)
8294 * @since 1.0.5
8295 *
8296 * @param FS_Entity $entity
8297 *
8298 * @return FS_Entity Return an decrypted clone entity.
8299 */
8300 private static function decrypt_entity( FS_Entity $entity ) {
8301 $clone = clone $entity;
8302 $props = get_object_vars( $entity );
8303
8304 foreach ( $props as $key => $val ) {
8305 $clone->{$key} = self::_decrypt( $val );
8306 }
8307
8308 return $clone;
8309 }
8310
8311 /**
8312 * Tries to activate account based on POST params.
8313 *
8314 * @author Vova Feldman (@svovaf)
8315 * @since 1.0.2
8316 */
8317 function _activate_account() {
8318 if ( $this->is_registered() ) {
8319 // Already activated.
8320 return;
8321 }
8322
8323 self::_clean_admin_content_section();
8324
8325 if ( fs_request_is_action( 'activate' ) && fs_request_is_post() ) {
8326 // check_admin_referer( 'activate_' . $this->_plugin->public_key );
8327
8328 // Verify matching plugin details.
8329 if ( $this->_plugin->id != fs_request_get( 'plugin_id' ) || $this->_slug != fs_request_get( 'plugin_slug' ) ) {
8330 return;
8331 }
8332
8333 $user = new FS_User();
8334 $user->id = fs_request_get( 'user_id' );
8335 $user->public_key = fs_request_get( 'user_public_key' );
8336 $user->secret_key = fs_request_get( 'user_secret_key' );
8337 $user->email = fs_request_get( 'user_email' );
8338 $user->first = fs_request_get( 'user_first' );
8339 $user->last = fs_request_get( 'user_last' );
8340 $user->is_verified = fs_request_get_bool( 'user_is_verified' );
8341
8342 $site = new FS_Site();
8343 $site->id = fs_request_get( 'install_id' );
8344 $site->public_key = fs_request_get( 'install_public_key' );
8345 $site->secret_key = fs_request_get( 'install_secret_key' );
8346 $site->plan->id = fs_request_get( 'plan_id' );
8347 $site->plan->title = fs_request_get( 'plan_title' );
8348 $site->plan->name = fs_request_get( 'plan_name' );
8349
8350 $plans = array();
8351 $plans_data = json_decode( urldecode( fs_request_get( 'plans' ) ) );
8352 foreach ( $plans_data as $p ) {
8353 $plans[] = new FS_Plugin_Plan( $p );
8354 }
8355
8356 $this->_set_account( $user, $site, $plans );
8357
8358 // Reload the page with the keys.
8359 fs_redirect( $this->_get_admin_page_url() );
8360 }
8361 }
8362
8363 /**
8364 * @author Vova Feldman (@svovaf)
8365 * @since 1.0.7
8366 *
8367 * @param string $email
8368 *
8369 * @return FS_User|bool
8370 */
8371 static function _get_user_by_email( $email ) {
8372 self::$_static_logger->entrance();
8373
8374 $email = trim( strtolower( $email ) );
8375 $users = self::get_all_users();
8376 if ( is_array( $users ) ) {
8377 foreach ( $users as $u ) {
8378 if ( $email === trim( strtolower( $u->email ) ) ) {
8379 return $u;
8380 }
8381 }
8382 }
8383
8384 return false;
8385 }
8386
8387 #----------------------------------------------------------------------------------
8388 #region Account (Loading, Updates & Activation)
8389 #----------------------------------------------------------------------------------
8390
8391 /***
8392 * Load account information (user + site).
8393 *
8394 * @author Vova Feldman (@svovaf)
8395 * @since 1.0.1
8396 */
8397 private function _load_account() {
8398 $this->_logger->entrance();
8399
8400 $this->do_action( 'before_account_load' );
8401
8402 $sites = self::get_all_sites( $this->_module_type );
8403 $users = self::get_all_users();
8404 $plans = self::get_all_plans( $this->_module_type );
8405 $licenses = self::get_all_licenses( $this->_module_type );
8406
8407 if ( $this->_logger->is_on() && is_admin() ) {
8408 $this->_logger->log( 'sites = ' . var_export( $sites, true ) );
8409 $this->_logger->log( 'users = ' . var_export( $users, true ) );
8410 $this->_logger->log( 'plans = ' . var_export( $plans, true ) );
8411 $this->_logger->log( 'licenses = ' . var_export( $licenses, true ) );
8412 }
8413
8414 $site = isset( $sites[ $this->_slug ] ) ? $sites[ $this->_slug ] : false;
8415
8416 if ( is_object( $site ) &&
8417 is_numeric( $site->id ) &&
8418 is_numeric( $site->user_id ) &&
8419 is_object( $site->plan )
8420 ) {
8421 // Load site.
8422 $this->_site = clone $site;
8423 $this->_site->plan = self::decrypt_entity( $this->_site->plan );
8424
8425 /**
8426 * If the install owner's details are not stored locally, use the previous user's details if available.
8427 *
8428 * @author Leo Fajardo (@leorw)
8429 */
8430 if ( ! isset( $users[ $this->_site->user_id ] ) && FS_User::is_valid_id( $this->_storage->prev_user_id ) ) {
8431 $user_id = $this->_storage->prev_user_id;
8432 } else {
8433 $user_id = $this->_site->user_id;
8434 }
8435
8436 // Load relevant user.
8437 $this->_user = clone $users[ $user_id ];
8438
8439 // Load plans.
8440 $this->_plans = $plans[ $this->_slug ];
8441 if ( ! is_array( $this->_plans ) || empty( $this->_plans ) ) {
8442 $this->_sync_plans();
8443 } else {
8444 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
8445 if ( $this->_plans[ $i ] instanceof FS_Plugin_Plan ) {
8446 $this->_plans[ $i ] = self::decrypt_entity( $this->_plans[ $i ] );
8447 } else {
8448 unset( $this->_plans[ $i ] );
8449 }
8450 }
8451 }
8452
8453 // Load licenses.
8454 $this->_licenses = array();
8455 if ( is_array( $licenses ) &&
8456 isset( $licenses[ $this->_slug ] ) &&
8457 isset( $licenses[ $this->_slug ][ $this->_user->id ] )
8458 ) {
8459 $this->_licenses = $licenses[ $this->_slug ][ $this->_user->id ];
8460 }
8461
8462 $this->_license = $this->_get_license_by_id( $this->_site->license_id );
8463
8464 if ( $this->_site->version != $this->get_plugin_version() ) {
8465 // If stored install version is different than current installed plugin version,
8466 // then update plugin version event.
8467 $this->update_plugin_version_event();
8468 }
8469 }
8470
8471 $this->_register_account_hooks();
8472 }
8473
8474 /**
8475 * @author Vova Feldman (@svovaf)
8476 * @since 1.0.1
8477 *
8478 * @param FS_User $user
8479 * @param FS_Site $site
8480 * @param bool|array $plans
8481 */
8482 private function _set_account( FS_User $user, FS_Site $site, $plans = false ) {
8483 $site->slug = $this->_slug;
8484 $site->user_id = $user->id;
8485
8486 $this->_site = $site;
8487 $this->_user = $user;
8488 if ( false !== $plans ) {
8489 $this->_plans = $plans;
8490 }
8491
8492 $this->send_install_update();
8493
8494 $this->_store_account();
8495
8496 }
8497
8498 /**
8499 * @author Vova Feldman (@svovaf)
8500 * @since 1.1.7.4
8501 *
8502 * @param array $override_with
8503 *
8504 * @return array
8505 */
8506 function get_opt_in_params( $override_with = array() ) {
8507 $this->_logger->entrance();
8508
8509 $current_user = self::_get_current_wp_user();
8510
8511 $activation_action = $this->get_unique_affix() . '_activate_new';
8512 $return_url = $this->is_anonymous() ?
8513 // If skipped already, then return to the account page.
8514 $this->get_account_url( $activation_action, array(), false ) :
8515 // Return to the module's main page.
8516 $this->get_after_activation_url( 'after_connect_url', array( 'fs_action' => $activation_action ) );
8517
8518 $params = array(
8519 'user_firstname' => $current_user->user_firstname,
8520 'user_lastname' => $current_user->user_lastname,
8521 'user_nickname' => $current_user->user_nicename,
8522 'user_email' => $current_user->user_email,
8523 'user_ip' => WP_FS__REMOTE_ADDR,
8524 'plugin_slug' => $this->_slug,
8525 'plugin_id' => $this->get_id(),
8526 'plugin_public_key' => $this->get_public_key(),
8527 'plugin_version' => $this->get_plugin_version(),
8528 'return_url' => fs_nonce_url( $return_url, $activation_action ),
8529 'account_url' => fs_nonce_url( $this->_get_admin_page_url(
8530 'account',
8531 array( 'fs_action' => 'sync_user' )
8532 ), 'sync_user' ),
8533 'site_uid' => $this->get_anonymous_id(),
8534 'site_url' => get_site_url(),
8535 'site_name' => get_bloginfo( 'name' ),
8536 'platform_version' => get_bloginfo( 'version' ),
8537 'sdk_version' => $this->version,
8538 'programming_language_version' => phpversion(),
8539 'language' => get_bloginfo( 'language' ),
8540 'charset' => get_bloginfo( 'charset' ),
8541 'is_premium' => $this->is_premium(),
8542 'is_active' => true,
8543 'is_uninstalled' => false,
8544 );
8545
8546 if ( $this->is_pending_activation() &&
8547 ! empty( $this->_storage->pending_license_key )
8548 ) {
8549 $params['license_key'] = $this->_storage->pending_license_key;
8550 }
8551
8552 if ( WP_FS__SKIP_EMAIL_ACTIVATION && $this->has_secret_key() ) {
8553 // Even though rand() is known for its security issues,
8554 // the timestamp adds another layer of protection.
8555 // It would be very hard for an attacker to get the secret key form here.
8556 // Plus, this should never run in production since the secret should never
8557 // be included in the production version.
8558 $params['ts'] = WP_FS__SCRIPT_START_TIME;
8559 $params['salt'] = md5( uniqid( rand() ) );
8560 $params['secure'] = md5(
8561 $params['ts'] .
8562 $params['salt'] .
8563 $this->get_secret_key()
8564 );
8565 }
8566
8567 return array_merge( $params, $override_with );
8568 }
8569
8570 /**
8571 * 1. If successful opt-in or pending activation returns the next page that the user should be redirected to.
8572 * 2. If there was an API error, return the API result.
8573 *
8574 * @author Vova Feldman (@svovaf)
8575 * @since 1.1.7.4
8576 *
8577 * @param string|bool $email
8578 * @param string|bool $first
8579 * @param string|bool $last
8580 * @param string|bool $license_key
8581 * @param bool $is_uninstall If "true", this means that the module is currently being uninstalled.
8582 * In this case, the user and site info will be sent to the server but no
8583 * data will be saved to the WP installation's database.
8584 * @param number|bool $trial_plan_id
8585 * @param bool $is_disconnected Whether or not to opt in without tracking.
8586 *
8587 * @return string|object
8588 * @use WP_Error
8589 */
8590 function opt_in(
8591 $email = false,
8592 $first = false,
8593 $last = false,
8594 $license_key = false,
8595 $is_uninstall = false,
8596 $trial_plan_id = false,
8597 $is_disconnected = false
8598 ) {
8599 $this->_logger->entrance();
8600
8601 if ( false === $email ) {
8602 $current_user = self::_get_current_wp_user();
8603 $email = $current_user->user_email;
8604 }
8605
8606 /**
8607 * @since 1.2.1 If activating with license key, ignore the context-user
8608 * since the user will be automatically loaded from the license.
8609 */
8610 if ( empty( $license_key ) ) {
8611 // Clean up pending license if opt-ing in again.
8612 $this->_storage->remove( 'pending_license_key' );
8613
8614 if ( ! $is_uninstall ) {
8615 $fs_user = Freemius::_get_user_by_email( $email );
8616 if ( is_object( $fs_user ) && ! $this->is_pending_activation() ) {
8617 return $this->install_with_current_user( false, $trial_plan_id );
8618 }
8619 }
8620 }
8621
8622 $user_info = array();
8623 if ( ! empty( $email ) ) {
8624 $user_info['user_email'] = $email;
8625 }
8626 if ( ! empty( $first ) ) {
8627 $user_info['user_firstname'] = $first;
8628 }
8629 if ( ! empty( $last ) ) {
8630 $user_info['user_lastname'] = $last;
8631 }
8632
8633 $params = $this->get_opt_in_params( $user_info );
8634
8635 $filtered_license_key = false;
8636 if ( is_string( $license_key ) ) {
8637 $filtered_license_key = $this->apply_filters( 'license_key', $license_key );
8638 $params['license_key'] = $filtered_license_key;
8639 } else if ( FS_Plugin_Plan::is_valid_id( $trial_plan_id ) ) {
8640 $params['trial_plan_id'] = $trial_plan_id;
8641 }
8642
8643 if ( $is_uninstall ) {
8644 $params['uninstall_params'] = array(
8645 'reason_id' => $this->_storage->uninstall_reason->id,
8646 'reason_info' => $this->_storage->uninstall_reason->info
8647 );
8648 }
8649
8650 if ( isset( $params['license_key'] ) ) {
8651 $fs_user = Freemius::_get_user_by_email( $email );
8652
8653 if ( is_object( $fs_user ) ) {
8654 /**
8655 * If opting in with a context license and the context WP Admin user already opted in
8656 * before from the current site, add the user context security params to avoid the
8657 * unnecessry email activation when the context license is owned by the same context user.
8658 *
8659 * @author Leo Fajardo (@leorw)
8660 * @since 1.2.3
8661 */
8662 $params = array_merge( $params, FS_Security::instance()->get_context_params(
8663 $fs_user,
8664 false,
8665 'install_with_existing_user'
8666 ) );
8667 }
8668 }
8669
8670 $params['is_disconnected'] = $is_disconnected;
8671 $params['format'] = 'json';
8672
8673 $url = WP_FS__ADDRESS . '/action/service/user/install/';
8674 if ( isset( $_COOKIE['XDEBUG_SESSION'] ) ) {
8675 $url = add_query_arg( 'XDEBUG_SESSION', 'PHPSTORM', $url );
8676 }
8677
8678 $response = wp_remote_post( $url, array(
8679 'method' => 'POST',
8680 'body' => $params,
8681 'timeout' => 15,
8682 ) );
8683
8684 if ( $response instanceof WP_Error ) {
8685 if ( 'https://' === substr( $url, 0, 8 ) &&
8686 isset( $response->errors ) &&
8687 isset( $response->errors['http_request_failed'] )
8688 ) {
8689 $http_error = strtolower( $response->errors['http_request_failed'][0] );
8690
8691 if ( false !== strpos( $http_error, 'ssl' ) ) {
8692 // Failed due to old version of cURL or Open SSL (SSLv3 is not supported by CloudFlare).
8693 $url = 'http://' . substr( $url, 8 );
8694
8695 $response = wp_remote_post( $url, array(
8696 'method' => 'POST',
8697 'body' => $params,
8698 'timeout' => 15,
8699 ) );
8700 }
8701 }
8702 }
8703
8704 if ( is_wp_error( $response ) ) {
8705 /**
8706 * @var WP_Error $response
8707 */
8708 $result = new stdClass();
8709
8710 $error_code = $response->get_error_code();
8711 $error_type = str_replace( ' ', '', ucwords( str_replace( '_', ' ', $error_code ) ) );
8712
8713 $result->error = (object) array(
8714 'type' => $error_type,
8715 'message' => $response->get_error_message(),
8716 'code' => $error_code,
8717 'http' => 402
8718 );
8719
8720 return $result;
8721 }
8722
8723 // Module is being uninstalled, don't handle the returned data.
8724 if ( $is_uninstall ) {
8725 return true;
8726 }
8727
8728 /**
8729 * 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.
8730 *
8731 * @author Vova Feldman (@svovaf)
8732 * @since 1.2.3
8733 * @link https://themes.trac.wordpress.org/ticket/46134#comment:5
8734 * @link https://themes.trac.wordpress.org/ticket/46134#comment:9
8735 * @link https://themes.trac.wordpress.org/ticket/46134#comment:12
8736 * @link https://themes.trac.wordpress.org/ticket/46134#comment:14
8737 */
8738 $decoded = is_string( $response['body'] ) ?
8739 json_decode( $response['body'] ) :
8740 null;
8741
8742 if ( empty( $decoded ) ) {
8743 return false;
8744 }
8745
8746 if ( ! $this->is_api_result_object( $decoded ) ) {
8747 if ( ! empty( $params['license_key'] ) ) {
8748 // Pass the fully entered license key to the failure handler.
8749 $params['license_key'] = $license_key;
8750 }
8751
8752 return $is_uninstall ?
8753 $decoded :
8754 $this->apply_filters( 'after_install_failure', $decoded, $params );
8755 } else if ( isset( $decoded->pending_activation ) && $decoded->pending_activation ) {
8756 // Pending activation, add message.
8757 return $this->set_pending_confirmation(
8758 ( isset( $decoded->email ) ?
8759 $decoded->email :
8760 true ),
8761 false,
8762 $filtered_license_key,
8763 ! empty( $params['trial_plan_id'] )
8764 );
8765 } else if ( isset( $decoded->install_secret_key ) ) {
8766 return $this->install_with_new_user(
8767 $decoded->user_id,
8768 $decoded->user_public_key,
8769 $decoded->user_secret_key,
8770 $decoded->install_id,
8771 $decoded->install_public_key,
8772 $decoded->install_secret_key,
8773 false
8774 );
8775 }
8776
8777 return $decoded;
8778 }
8779
8780 /**
8781 * Set user and site identities.
8782 *
8783 * @author Vova Feldman (@svovaf)
8784 * @since 1.0.9
8785 *
8786 * @param FS_User $user
8787 * @param FS_Site $site
8788 * @param bool $redirect
8789 * @param bool $auto_install Since 1.2.1.7 If `true` and setting up an account with a valid license, will
8790 * redirect (or return a URL) to the account page with a special parameter to
8791 * trigger the auto installation processes.
8792 *
8793 * @return string If redirect is `false`, returns the next page the user should be redirected to.
8794 */
8795 function setup_account(
8796 FS_User $user,
8797 FS_Site $site,
8798 $redirect = true,
8799 $auto_install = false
8800 ) {
8801 $this->_user = $user;
8802 $this->_site = $site;
8803
8804 $this->_sync_plans();
8805
8806 $this->_enrich_site_plan( false );
8807
8808 $this->_set_account( $user, $site );
8809
8810 if ( $this->is_trial() ) {
8811 // Store trial plan information.
8812 $this->_enrich_site_trial_plan( true );
8813 }
8814
8815 // If Freemius was OFF before, turn it on.
8816 $this->turn_on();
8817
8818 $this->do_action( 'after_account_connection', $user, $site );
8819
8820 if ( is_numeric( $site->license_id ) ) {
8821 $this->_license = $this->_get_license_by_id( $site->license_id );
8822 }
8823
8824 $this->_admin_notices->remove_sticky( 'connect_account' );
8825
8826 if ( $this->is_pending_activation() || ! $this->has_settings_menu() ) {
8827 // Remove pending activation sticky notice (if still exist).
8828 $this->_admin_notices->remove_sticky( 'activation_pending' );
8829
8830 // Remove plugin from pending activation mode.
8831 unset( $this->_storage->is_pending_activation );
8832
8833 if ( ! $this->is_paying_or_trial() ) {
8834 $this->_admin_notices->add_sticky(
8835 sprintf( $this->get_text_x_inline( '%s activation was successfully completed.',
8836 'pluginX activation was successfully...', 'plugin-x-activation-message' ), '<b>' . $this->get_plugin_name() . '</b>' ),
8837 'activation_complete'
8838 );
8839 }
8840 }
8841
8842 if ( $this->is_paying_or_trial() ) {
8843 if ( ! $this->is_premium() || ! $this->has_premium_version() || ! $this->has_settings_menu() ) {
8844 if ( $this->is_paying() ) {
8845 $this->_admin_notices->add_sticky(
8846 sprintf(
8847 $this->get_text_inline( 'Your account was successfully activated with the %s plan.', 'activation-with-plan-x-message' ),
8848 $this->_site->plan->title
8849 ) . $this->get_complete_upgrade_instructions(),
8850 'plan_upgraded',
8851 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
8852 );
8853 } else {
8854 $this->_admin_notices->add_sticky(
8855 sprintf(
8856 $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ),
8857 '<i>' . $this->get_plugin_name() . '</i>'
8858 ) . $this->get_complete_upgrade_instructions( $this->_storage->trial_plan->title ),
8859 'trial_started',
8860 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
8861 );
8862 }
8863 }
8864
8865 $this->_admin_notices->remove_sticky( array(
8866 'trial_promotion',
8867 ) );
8868 }
8869
8870 $plugin_id = fs_request_get( 'plugin_id', false );
8871
8872 // Store activation time ONLY for plugins (not add-ons).
8873 if ( ! is_numeric( $plugin_id ) || ( $plugin_id == $this->_plugin->id ) ) {
8874 $this->_storage->activation_timestamp = WP_FS__SCRIPT_START_TIME;
8875 }
8876
8877 $next_page = '';
8878
8879 $extra = array();
8880 if ( $auto_install ) {
8881 $extra['auto_install'] = 'true';
8882 }
8883
8884 if ( is_numeric( $plugin_id ) ) {
8885 /**
8886 * @author Leo Fajardo
8887 * @since 1.2.1.6
8888 *
8889 * Also sync the license after an anonymous user subscribes.
8890 */
8891 if ( $this->is_anonymous() || $plugin_id != $this->_plugin->id ) {
8892 // Add-on was installed - sync license right after install.
8893 $next_page = $this->_get_sync_license_url( $plugin_id, true, $extra );
8894 }
8895 } else {
8896 /**
8897 * @author Vova Feldman (@svovaf)
8898 * @since 1.1.9 If site installed with a valid license, sync license.
8899 */
8900 if ( $this->is_paying() ) {
8901 $this->_sync_plugin_license( true );
8902 }
8903
8904 // Reload the page with the keys.
8905 $next_page = $this->is_anonymous() ?
8906 // If user previously skipped, redirect to account page.
8907 $this->get_account_url( false, $extra ) :
8908 $this->get_after_activation_url( 'after_connect_url' );
8909 }
8910
8911 if ( ! empty( $next_page ) && $redirect ) {
8912 fs_redirect( $next_page );
8913 }
8914
8915 return $next_page;
8916 }
8917
8918 /**
8919 * Install plugin with new user information after approval.
8920 *
8921 * @author Vova Feldman (@svovaf)
8922 * @since 1.0.7
8923 */
8924 function _install_with_new_user() {
8925 $this->_logger->entrance();
8926
8927 if ( $this->is_registered() ) {
8928 return;
8929 }
8930
8931 if ( ( $this->is_plugin() && fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) ||
8932 // @todo This logic should be improved because it's executed on every load of a theme.
8933 $this->is_theme()
8934 ) {
8935 // check_admin_referer( $this->_slug . '_activate_new' );
8936
8937 if ( fs_request_has( 'user_secret_key' ) ) {
8938 $this->install_with_new_user(
8939 fs_request_get( 'user_id' ),
8940 fs_request_get( 'user_public_key' ),
8941 fs_request_get( 'user_secret_key' ),
8942 fs_request_get( 'install_id' ),
8943 fs_request_get( 'install_public_key' ),
8944 fs_request_get( 'install_secret_key' ),
8945 true,
8946 fs_request_get_bool( 'auto_install' )
8947 );
8948 } else if ( fs_request_has( 'pending_activation' ) ) {
8949 $this->set_pending_confirmation( fs_request_get( 'user_email' ), true );
8950 }
8951 }
8952 }
8953
8954 /**
8955 * Install plugin with new user.
8956 *
8957 * @author Vova Feldman (@svovaf)
8958 * @since 1.1.7.4
8959 *
8960 * @param number $user_id
8961 * @param string $user_public_key
8962 * @param string $user_secret_key
8963 * @param number $install_id
8964 * @param string $install_public_key
8965 * @param string $install_secret_key
8966 * @param bool $redirect
8967 * @param bool $auto_install Since 1.2.1.7 If `true` and setting up an account with a valid license, will
8968 * redirect (or return a URL) to the account page with a special parameter to
8969 * trigger the auto installation processes.
8970 *
8971 * @return string If redirect is `false`, returns the next page the user should be redirected to.
8972 */
8973 private function install_with_new_user(
8974 $user_id,
8975 $user_public_key,
8976 $user_secret_key,
8977 $install_id,
8978 $install_public_key,
8979 $install_secret_key,
8980 $redirect = true,
8981 $auto_install = false
8982 ) {
8983 $user = new FS_User();
8984 $user->id = $user_id;
8985 $user->public_key = $user_public_key;
8986 $user->secret_key = $user_secret_key;
8987
8988 $this->_user = $user;
8989 $user_result = $this->get_api_user_scope()->get();
8990 $user = new FS_User( $user_result );
8991 $this->_user = $user;
8992
8993 $site = new FS_Site();
8994 $site->id = $install_id;
8995 $site->public_key = $install_public_key;
8996 $site->secret_key = $install_secret_key;
8997
8998 $this->_site = $site;
8999 $site_result = $this->get_api_site_scope()->get();
9000 $site = new FS_Site( $site_result );
9001 $this->_site = $site;
9002
9003 return $this->setup_account(
9004 $this->_user,
9005 $this->_site,
9006 $redirect,
9007 $auto_install
9008 );
9009 }
9010
9011 /**
9012 * @author Vova Feldman (@svovaf)
9013 * @since 1.1.7.4
9014 *
9015 * @param string|bool $email
9016 * @param bool $redirect
9017 * @param string|bool $license_key Since 1.2.1.5
9018 * @param bool $is_pending_trial Since 1.2.1.5
9019 *
9020 * @return string Since 1.2.1.5 if $redirect is `false`, return the pending activation page.
9021 */
9022 private function set_pending_confirmation(
9023 $email = false,
9024 $redirect = true,
9025 $license_key = false,
9026 $is_pending_trial = false
9027 ) {
9028 if ( $this->_ignore_pending_mode ) {
9029 /**
9030 * If explicitly asked to ignore pending mode, set to anonymous mode
9031 * if require confirmation before finalizing the opt-in.
9032 *
9033 * @author Vova Feldman
9034 * @since 1.2.1.6
9035 */
9036 $this->skip_connection();
9037 } else {
9038 // Install must be activated via email since
9039 // user with the same email already exist.
9040 $this->_storage->is_pending_activation = true;
9041 $this->_add_pending_activation_notice( $email, $is_pending_trial );
9042 }
9043
9044 if ( ! empty( $license_key ) ) {
9045 $this->_storage->pending_license_key = $license_key;
9046 }
9047
9048 // Remove the opt-in sticky notice.
9049 $this->_admin_notices->remove_sticky( array(
9050 'connect_account',
9051 'trial_promotion',
9052 ) );
9053
9054 $next_page = $this->get_after_activation_url( 'after_pending_connect_url' );
9055
9056 // Reload the page with with pending activation message.
9057 if ( $redirect ) {
9058 fs_redirect( $next_page );
9059 }
9060
9061 return $next_page;
9062 }
9063
9064 /**
9065 * Install plugin with current logged WP user info.
9066 *
9067 * @author Vova Feldman (@svovaf)
9068 * @since 1.0.7
9069 */
9070 function _install_with_current_user() {
9071 $this->_logger->entrance();
9072
9073 if ( $this->is_registered() ) {
9074 return;
9075 }
9076
9077 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) && fs_request_is_post() ) {
9078 // check_admin_referer( 'activate_existing_' . $this->_plugin->public_key );
9079
9080 /**
9081 * @author Vova Feldman (@svovaf)
9082 * @since 1.1.9 Add license key if given.
9083 */
9084 $license_key = fs_request_get( 'license_secret_key' );
9085
9086 $this->install_with_current_user( $license_key );
9087 }
9088 }
9089
9090
9091 /**
9092 * @author Vova Feldman (@svovaf)
9093 * @since 1.1.7.4
9094 *
9095 * @param string|bool $license_key
9096 * @param number|bool $trial_plan_id
9097 * @param bool $redirect
9098 *
9099 * @return string|object If redirect is `false`, returns the next page the user should be redirected to, or the
9100 * API error object if failed to install.
9101 */
9102 private function install_with_current_user(
9103 $license_key = false,
9104 $trial_plan_id = false,
9105 $redirect = true
9106 ) {
9107 // Get current logged WP user.
9108 $current_user = self::_get_current_wp_user();
9109
9110 // Find the relevant FS user by the email.
9111 $user = self::_get_user_by_email( $current_user->user_email );
9112
9113 // We have to set the user before getting user scope API handler.
9114 $this->_user = $user;
9115
9116 $extra_install_params = array(
9117 'uid' => $this->get_anonymous_id(),
9118 );
9119
9120 if ( ! empty( $license_key ) ) {
9121 $filtered_license_key = $this->apply_filters( 'license_key', $license_key );
9122 $extra_install_params['license_key'] = $filtered_license_key;
9123 } else if ( FS_Plugin_Plan::is_valid_id( $trial_plan_id ) ) {
9124 $extra_install_params['trial_plan_id'] = $trial_plan_id;
9125 }
9126
9127 $args = $this->get_install_data_for_api( $extra_install_params, false, false );
9128
9129 // Install the plugin.
9130 $install = $this->get_api_user_scope()->call(
9131 "/plugins/{$this->get_id()}/installs.json",
9132 'post',
9133 $args
9134 );
9135
9136 if ( ! $this->is_api_result_entity( $install ) ) {
9137 if ( ! empty( $args['license_key'] ) ) {
9138 // Pass full the fully entered license key to the failure handler.
9139 $args['license_key'] = $license_key;
9140 }
9141
9142 $install = $this->apply_filters( 'after_install_failure', $install, $args );
9143
9144 $this->_admin_notices->add(
9145 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
9146 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $install->error->message . '</b>',
9147 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
9148 'error'
9149 );
9150
9151 if ( $redirect ) {
9152 /**
9153 * We set the user before getting the user scope API handler, so the user became temporarily
9154 * registered (`is_registered() = true`). Since the API returned an error and we will redirect,
9155 * we have to set the user to `null`, otherwise, the user will be redirected to the wrong
9156 * activation page based on the return value of `is_registered()`. In addition, in case the
9157 * context plugin doesn't have a settings menu and the default page is the `Plugins` page,
9158 * misleading plugin activation errors will be shown on the `Plugins` page.
9159 *
9160 * @author Leo Fajardo (@leorw)
9161 */
9162 $this->_user = null;
9163
9164 fs_redirect( $this->get_activation_url( array( 'error' => $install->error->message ) ) );
9165 }
9166
9167 return $install;
9168 }
9169
9170 $site = new FS_Site( $install );
9171 $this->_site = $site;
9172
9173 return $this->setup_account( $this->_user, $this->_site, $redirect );
9174 }
9175
9176 /**
9177 * Tries to activate add-on account based on parent plugin info.
9178 *
9179 * @author Vova Feldman (@svovaf)
9180 * @since 1.0.6
9181 *
9182 * @param Freemius $parent_fs
9183 */
9184 private function _activate_addon_account( Freemius $parent_fs ) {
9185 if ( $this->is_registered() ) {
9186 // Already activated.
9187 return;
9188 }
9189
9190 // Activate add-on with parent plugin credentials.
9191 $addon_install = $parent_fs->get_api_site_scope()->call(
9192 "/addons/{$this->_plugin->id}/installs.json",
9193 'post',
9194 $this->get_install_data_for_api( array(
9195 'uid' => $this->get_anonymous_id(),
9196 ), false, false )
9197 );
9198
9199 if ( isset( $addon_install->error ) ) {
9200 $this->_admin_notices->add(
9201 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
9202 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $addon_install->error->message . '</b>',
9203 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
9204 'error'
9205 );
9206
9207 return;
9208 }
9209
9210 // First of all, set site info - otherwise we won't
9211 // be able to invoke API calls.
9212 $this->_site = new FS_Site( $addon_install );
9213
9214 // Sync add-on plans.
9215 $this->_sync_plans();
9216
9217 // Get site's current plan.
9218 $this->_site->plan = $this->_get_plan_by_id( $this->_site->plan->id );
9219
9220 // Get user information based on parent's plugin.
9221 $user = $parent_fs->get_user();
9222
9223 $this->_set_account( $user, $this->_site );
9224
9225 // Sync licenses.
9226 $this->_sync_licenses();
9227
9228 // Try to activate premium license.
9229 $this->_activate_license( true );
9230 }
9231
9232 /**
9233 * Tries to activate parent account based on add-on's info.
9234 *
9235 * @author Vova Feldman (@svovaf)
9236 * @since 1.2.2.7
9237 *
9238 * @param Freemius $parent_fs
9239 */
9240 private function activate_parent_account( Freemius $parent_fs ) {
9241 if ( ! $this->is_addon() ) {
9242 // This is not an add-on.
9243 return;
9244 }
9245
9246 if ( $parent_fs->is_registered() ) {
9247 // Already activated.
9248 return;
9249 }
9250
9251 // Activate parent with add-on's user credentials.
9252 $parent_install = $this->get_api_user_scope()->call(
9253 "/plugins/{$parent_fs->_plugin->id}/installs.json",
9254 'post',
9255 $parent_fs->get_install_data_for_api( array(
9256 'uid' => $parent_fs->get_anonymous_id(),
9257 ), false, false )
9258 );
9259
9260 if ( isset( $parent_install->error ) ) {
9261 $this->_admin_notices->add(
9262 sprintf( $this->get_text_inline( 'Couldn\'t activate %s.', 'could-not-activate-x' ), $this->get_plugin_name() ) . ' ' .
9263 $this->get_text_inline( 'Please contact us with the following message:', 'contact-us-with-error-message' ) . ' ' . '<b>' . $parent_install->error->message . '</b>',
9264 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
9265 'error'
9266 );
9267
9268 return;
9269 }
9270
9271 $parent_fs->_admin_notices->remove_sticky( 'connect_account' );
9272
9273 if ( $parent_fs->is_pending_activation() ) {
9274 $parent_fs->_admin_notices->remove_sticky( 'activation_pending' );
9275
9276 unset( $parent_fs->_storage->is_pending_activation );
9277 }
9278
9279 // First of all, set site info - otherwise we won't
9280 // be able to invoke API calls.
9281 $parent_fs->_site = new FS_Site( $parent_install );
9282
9283 // Sync add-on plans.
9284 $parent_fs->_sync_plans();
9285
9286 // Get site's current plan.
9287 $parent_fs->_site->plan = $parent_fs->_get_plan_by_id( $parent_fs->_site->plan->id );
9288
9289 // Get user information based on parent's plugin.
9290 $user = $this->get_user();
9291
9292 $parent_fs->_set_account( $user, $parent_fs->_site );
9293 }
9294
9295 #endregion
9296
9297 #----------------------------------------------------------------------------------
9298 #region Admin Menu Items
9299 #----------------------------------------------------------------------------------
9300
9301 private $_menu_items = array();
9302
9303 /**
9304 * @author Vova Feldman (@svovaf)
9305 * @since 1.2.1.8
9306 *
9307 * @return array
9308 */
9309 function get_menu_items() {
9310 return $this->_menu_items;
9311 }
9312
9313 /**
9314 * @author Vova Feldman (@svovaf)
9315 * @since 1.0.7
9316 *
9317 * @return string
9318 */
9319 function get_menu_slug() {
9320 return $this->_menu->get_slug();
9321 }
9322
9323 /**
9324 * @author Vova Feldman (@svovaf)
9325 * @since 1.0.9
9326 */
9327 function _prepare_admin_menu() {
9328 // if ( ! $this->is_on() ) {
9329 // return;
9330 // }
9331
9332 if ( ! $this->has_api_connectivity() && ! $this->is_enable_anonymous() ) {
9333 $this->_menu->remove_menu_item();
9334 } else {
9335 $this->do_action( 'before_admin_menu_init' );
9336
9337 $this->add_menu_action();
9338 $this->add_submenu_items();
9339 }
9340 }
9341
9342 /**
9343 * Admin dashboard menu items modifications.
9344 *
9345 * NOTE: admin_menu action executed before admin_init.
9346 *
9347 * @author Vova Feldman (@svovaf)
9348 * @since 1.0.7
9349 *
9350 */
9351 private function add_menu_action() {
9352 if ( $this->is_activation_mode() ) {
9353 if ( $this->is_plugin() || ( $this->has_settings_menu() && ! $this->is_free_wp_org_theme() ) ) {
9354 $this->override_plugin_menu_with_activation();
9355 } else {
9356 /**
9357 * Handle theme opt-in when the opt-in form shows as a dialog box in the themes page.
9358 */
9359 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) ) {
9360 add_action( 'load-themes.php', array( &$this, '_install_with_current_user' ) );
9361 } else if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ||
9362 fs_request_get_bool( 'pending_activation' )
9363 ) {
9364 add_action( 'load-themes.php', array( &$this, '_install_with_new_user' ) );
9365 }
9366 }
9367 } else {
9368 if ( ! $this->is_registered() ) {
9369 // If not registered try to install user.
9370 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) {
9371 $this->_install_with_new_user();
9372 }
9373 } else if (
9374 fs_request_is_action( 'sync_user' ) &&
9375 ( ! $this->has_settings_menu() || $this->is_free_wp_org_theme() )
9376 ) {
9377 $this->_handle_account_user_sync();
9378 }
9379 }
9380 }
9381
9382 /**
9383 * @author Vova Feldman (@svovaf)
9384 * @since 1.0.1
9385 */
9386 function _redirect_on_clicked_menu_link() {
9387 $this->_logger->entrance();
9388
9389 $page = strtolower( isset( $_REQUEST['page'] ) ? $_REQUEST['page'] : '' );
9390
9391 $this->_logger->log( 'page = ' . $page );
9392
9393 foreach ( $this->_menu_items as $priority => $items ) {
9394 foreach ( $items as $item ) {
9395 if ( isset( $item['url'] ) ) {
9396 if ( $page === $this->_menu->get_slug( strtolower( $item['menu_slug'] ) ) ) {
9397 $this->_logger->log( 'Redirecting to ' . $item['url'] );
9398
9399 fs_redirect( $item['url'] );
9400 }
9401 }
9402 }
9403 }
9404 }
9405
9406 /**
9407 * Remove plugin's all admin menu items & pages, and replace with activation page.
9408 *
9409 * @author Vova Feldman (@svovaf)
9410 * @since 1.0.1
9411 */
9412 private function override_plugin_menu_with_activation() {
9413 $this->_logger->entrance();
9414
9415 $hook = false;
9416
9417 if ( ! $this->_menu->has_menu() ) {
9418 // Add the opt-in page without a menu item.
9419 $hook = FS_Admin_Menu_Manager::add_subpage(
9420 null,
9421 $this->get_plugin_name(),
9422 $this->get_plugin_name(),
9423 'manage_options',
9424 $this->_slug,
9425 array( &$this, '_connect_page_render' )
9426 );
9427 } else if ( $this->_menu->is_top_level() ) {
9428 $hook = $this->_menu->override_menu_item( array( &$this, '_connect_page_render' ) );
9429
9430 if ( false === $hook ) {
9431 // Create new menu item just for the opt-in.
9432 $hook = FS_Admin_Menu_Manager::add_page(
9433 $this->get_plugin_name(),
9434 $this->get_plugin_name(),
9435 'manage_options',
9436 $this->_menu->get_slug(),
9437 array( &$this, '_connect_page_render' )
9438 );
9439 }
9440 } else {
9441 $menus = array( $this->_menu->get_parent_slug() );
9442
9443 if ( $this->_menu->is_override_exact() ) {
9444 // Make sure the current page is matching the activation page.
9445 if ( ! $this->is_matching_url( $this->get_activation_url() ) ) {
9446 return;
9447 }
9448 }
9449
9450 foreach ( $menus as $parent_slug ) {
9451 $hook = $this->_menu->override_submenu_action(
9452 $parent_slug,
9453 $this->_menu->get_raw_slug(),
9454 array( &$this, '_connect_page_render' )
9455 );
9456
9457 if ( false !== $hook ) {
9458 // Found plugin's submenu item.
9459 break;
9460 }
9461 }
9462 }
9463
9464 if ( $this->is_activation_page() ) {
9465 // Clean admin page from distracting content.
9466 self::_clean_admin_content_section();
9467 }
9468
9469 if ( false !== $hook ) {
9470 if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) ) {
9471 $this->_install_with_current_user();
9472 } else if ( fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) {
9473 $this->_install_with_new_user();
9474 }
9475 }
9476 }
9477
9478 /**
9479 * @author Leo Fajardo (leorw)
9480 * @since 1.2.1
9481 *
9482 * return string
9483 */
9484 function get_top_level_menu_capability() {
9485 global $menu;
9486
9487 $top_level_menu_slug = $this->get_top_level_menu_slug();
9488
9489 foreach ( $menu as $menu_info ) {
9490 /**
9491 * The second element in the menu info array is the capability/role that has access to the menu and the
9492 * third element is the menu slug.
9493 */
9494 if ( $menu_info[2] === $top_level_menu_slug ) {
9495 return $menu_info[1];
9496 }
9497 }
9498
9499 return 'read';
9500 }
9501
9502 /**
9503 * @author Vova Feldman (@svovaf)
9504 * @since 1.0.0
9505 *
9506 * @return string
9507 */
9508 private function get_top_level_menu_slug() {
9509 return ( $this->is_addon() ?
9510 $this->get_parent_instance()->_menu->get_top_level_menu_slug() :
9511 $this->_menu->get_top_level_menu_slug() );
9512 }
9513
9514 /**
9515 * @author Vova Feldman (@svovaf)
9516 * @since 1.2.2.7
9517 *
9518 * @return string
9519 */
9520 function get_pricing_cta_label() {
9521 $label = $this->get_text_inline( 'Upgrade', 'upgrade' );
9522
9523 if ( $this->is_in_trial_promotion() &&
9524 ! $this->is_paying_or_trial()
9525 ) {
9526 // If running a trial promotion, modify the pricing to load the trial.
9527 $label = $this->get_text_inline( 'Start Trial', 'start-trial' );
9528 } else if ( $this->is_paying() ) {
9529 $label = $this->get_text_inline( 'Pricing', 'pricing' );
9530 }
9531
9532 return $label;
9533 }
9534
9535 /**
9536 * @author Vova Feldman (@svovaf)
9537 * @since 1.2.2.7
9538 *
9539 * @return bool
9540 */
9541 function is_pricing_page_visible() {
9542 return (
9543 // Has at least one paid plan.
9544 $this->has_paid_plan() &&
9545 // Didn't ask to hide the pricing page.
9546 $this->is_page_visible( 'pricing' ) &&
9547 // Don't have a valid active license or has more than one plan.
9548 ( ! $this->is_paying() || ! $this->is_single_plan() )
9549 );
9550 }
9551
9552 /**
9553 * Add default Freemius menu items.
9554 *
9555 * @author Vova Feldman (@svovaf)
9556 * @since 1.0.0
9557 */
9558 private function add_submenu_items() {
9559 $this->_logger->entrance();
9560
9561 if ( ! $this->is_addon() ) {
9562 /**
9563 * @since 1.2.2.7 Also add submenu items when running in a free .org theme so the tabs will be visible.
9564 */
9565 if ( ! $this->is_activation_mode() || $this->is_free_wp_org_theme() ) {
9566 if ( $this->has_affiliate_program() ) {
9567 // Add affiliation page.
9568 $this->add_submenu_item(
9569 $this->get_text_inline( 'Affiliation', 'affiliation' ),
9570 array( &$this, '_affiliation_page_render' ),
9571 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Affiliation', 'affiliation' ),
9572 'manage_options',
9573 'affiliation',
9574 'Freemius::_clean_admin_content_section',
9575 WP_FS__DEFAULT_PRIORITY,
9576 $this->is_submenu_item_visible( 'affiliation' )
9577 );
9578 }
9579
9580 if ( $this->is_registered() ) {
9581 $show_account = (
9582 $this->is_submenu_item_visible( 'account' ) &&
9583 /**
9584 * @since 1.2.2.7 Don't show the Account for free WP.org themes without any paid plans.
9585 */
9586 ( ! $this->is_free_wp_org_theme() || $this->has_paid_plan() )
9587 );
9588
9589 // Add user account page.
9590 $this->add_submenu_item(
9591 $this->get_text_inline( 'Account', 'account' ),
9592 array( &$this, '_account_page_render' ),
9593 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Account', 'account' ),
9594 'manage_options',
9595 'account',
9596 array( &$this, '_account_page_load' ),
9597 WP_FS__DEFAULT_PRIORITY,
9598 $show_account
9599 );
9600 }
9601
9602 // Add contact page.
9603 $this->add_submenu_item(
9604 $this->get_text_inline( 'Contact Us', 'contact-us' ),
9605 array( &$this, '_contact_page_render' ),
9606 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Contact Us', 'contact-us' ),
9607 'manage_options',
9608 'contact',
9609 'Freemius::_clean_admin_content_section',
9610 WP_FS__DEFAULT_PRIORITY,
9611 $this->is_submenu_item_visible( 'contact' )
9612 );
9613
9614 if ( $this->has_addons() ) {
9615 $this->add_submenu_item(
9616 $this->get_text_inline( 'Add-Ons', 'add-ons' ),
9617 array( &$this, '_addons_page_render' ),
9618 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_inline( 'Add-Ons', 'add-ons' ),
9619 'manage_options',
9620 'addons',
9621 array( &$this, '_addons_page_load' ),
9622 WP_FS__LOWEST_PRIORITY - 1,
9623 $this->is_submenu_item_visible( 'addons' )
9624 );
9625 }
9626
9627 $show_pricing = (
9628 $this->is_submenu_item_visible( 'pricing' ) &&
9629 $this->is_pricing_page_visible()
9630 );
9631
9632 $pricing_cta_text = $this->get_pricing_cta_label();
9633 $pricing_class = 'upgrade-mode';
9634 if ( $show_pricing ) {
9635 if ( $this->is_in_trial_promotion() &&
9636 ! $this->is_paying_or_trial()
9637 ) {
9638 // If running a trial promotion, modify the pricing to load the trial.
9639 $pricing_class = 'trial-mode';
9640 } else if ( $this->is_paying() ) {
9641 $pricing_class = '';
9642 }
9643 }
9644
9645 // Add upgrade/pricing page.
9646 $this->add_submenu_item(
9647 $pricing_cta_text . '&nbsp;&nbsp;' . ( is_rtl() ? '&#x2190;' : '&#x27a4;' ),
9648 array( &$this, '_pricing_page_render' ),
9649 $this->get_plugin_name() . ' &ndash; ' . $this->get_text_x_inline( 'Pricing', 'noun', 'pricing' ),
9650 'manage_options',
9651 'pricing',
9652 'Freemius::_clean_admin_content_section',
9653 WP_FS__LOWEST_PRIORITY,
9654 $show_pricing,
9655 $pricing_class
9656 );
9657 }
9658 }
9659
9660
9661 if ( 0 < count( $this->_menu_items ) ) {
9662 if ( ! $this->_menu->is_top_level() ) {
9663 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
9664
9665 // Append submenu items right after the plugin's submenu item.
9666 $this->order_sub_submenu_items();
9667 } else {
9668 // Append submenu items.
9669 $this->embed_submenu_items();
9670 }
9671 }
9672 }
9673
9674 /**
9675 * Moved the actual submenu item additions to a separated function,
9676 * in order to support sub-submenu items when the plugin's settings
9677 * only have a submenu and not top-level menu item.
9678 *
9679 * @author Vova Feldman (@svovaf)
9680 * @since 1.1.4
9681 */
9682 private function embed_submenu_items() {
9683 $item_template = $this->_menu->is_top_level() ?
9684 '<span class="fs-submenu-item %s %s %s">%s</span>' :
9685 '<span class="fs-submenu-item fs-sub %s %s %s">%s</span>';
9686
9687 $top_level_menu_capability = $this->get_top_level_menu_capability();
9688
9689 ksort( $this->_menu_items );
9690
9691 foreach ( $this->_menu_items as $priority => $items ) {
9692 foreach ( $items as $item ) {
9693 $capability = ( ! empty( $item['capability'] ) ? $item['capability'] : $top_level_menu_capability );
9694
9695 $menu_item = sprintf(
9696 $item_template,
9697 $this->get_unique_affix(),
9698 $item['menu_slug'],
9699 ! empty( $item['class'] ) ? $item['class'] : '',
9700 $item['menu_title']
9701 );
9702
9703 $menu_slug = $this->_menu->get_slug( $item['menu_slug'] );
9704
9705 if ( ! isset( $item['url'] ) ) {
9706 $hook = FS_Admin_Menu_Manager::add_subpage(
9707 $item['show_submenu'] ?
9708 $this->get_top_level_menu_slug() :
9709 null,
9710 $item['page_title'],
9711 $menu_item,
9712 $capability,
9713 $menu_slug,
9714 $item['render_function']
9715 );
9716
9717 if ( false !== $item['before_render_function'] ) {
9718 add_action( "load-$hook", $item['before_render_function'] );
9719 }
9720 } else {
9721 FS_Admin_Menu_Manager::add_subpage(
9722 $item['show_submenu'] ?
9723 $this->get_top_level_menu_slug() :
9724 null,
9725 $item['page_title'],
9726 $menu_item,
9727 $capability,
9728 $menu_slug,
9729 array( $this, '' )
9730 );
9731 }
9732 }
9733 }
9734 }
9735
9736 /**
9737 * Re-order the submenu items so all Freemius added new submenu items
9738 * are added right after the plugin's settings submenu item.
9739 *
9740 * @author Vova Feldman (@svovaf)
9741 * @since 1.1.4
9742 */
9743 private function order_sub_submenu_items() {
9744 global $submenu;
9745
9746 $menu_slug = $this->_menu->get_top_level_menu_slug();
9747
9748 /**
9749 * Before "admin_menu" fires, WordPress will loop over the default submenus and remove pages for which the user
9750 * does not have permissions. So in case a plugin does not have top-level menu but does have submenus under any
9751 * of the default menus, only users that have the right role can access its sub-submenus (Account, Contact Us,
9752 * Support Forum, etc.) since $submenu[ $menu_slug ] will be empty if the user doesn't have permission.
9753 *
9754 * In case a plugin does not have submenus under any of the default menus but does have submenus under the menu
9755 * of another plugin, only users that have the right role can access its sub-submenus since we will use the
9756 * capability needed to access the parent menu as the capability for the submenus that we will add.
9757 */
9758 if ( empty( $submenu[ $menu_slug ] ) ) {
9759 return;
9760 }
9761
9762 $top_level_menu = &$submenu[ $menu_slug ];
9763
9764 $all_submenu_items_after = array();
9765
9766 $found_submenu_item = false;
9767
9768 foreach ( $top_level_menu as $submenu_id => $meta ) {
9769 if ( $found_submenu_item ) {
9770 // Remove all submenu items after the plugin's submenu item.
9771 $all_submenu_items_after[] = $meta;
9772 unset( $top_level_menu[ $submenu_id ] );
9773 }
9774
9775 if ( $this->_menu->get_raw_slug() === $meta[2] ) {
9776 // Found the submenu item, put all below.
9777 $found_submenu_item = true;
9778 continue;
9779 }
9780 }
9781
9782 // Embed all plugin's new submenu items.
9783 $this->embed_submenu_items();
9784
9785 // Start with specially high number to make sure it's appended.
9786 $i = max( 10000, max( array_keys( $top_level_menu ) ) + 1 );
9787 foreach ( $all_submenu_items_after as $meta ) {
9788 $top_level_menu[ $i ] = $meta;
9789 $i ++;
9790 }
9791
9792 // Sort submenu items.
9793 ksort( $top_level_menu );
9794 }
9795
9796 /**
9797 * Helper method to return the module's support forum URL.
9798 *
9799 * @author Vova Feldman (@svovaf)
9800 * @since 1.2.2.7
9801 *
9802 * @return string
9803 */
9804 function get_support_forum_url() {
9805 return $this->apply_filters( 'support_forum_url', "https://wordpress.org/support/{$this->_module_type}/{$this->_slug}" );
9806 }
9807
9808 /**
9809 * Displays the Support Forum link when enabled.
9810 *
9811 * Can be filtered like so:
9812 *
9813 * function _fs_show_support_menu( $is_visible, $menu_id ) {
9814 * if ( 'support' === $menu_id ) {
9815 * return _fs->is_registered();
9816 * }
9817 * return $is_visible;
9818 * }
9819 * _fs()->add_filter('is_submenu_visible', '_fs_show_support_menu', 10, 2);
9820 *
9821 */
9822 function _add_default_submenu_items() {
9823 if ( ! $this->is_on() ) {
9824 return;
9825 }
9826
9827 if ( ! $this->is_activation_mode() ) {
9828 $this->add_submenu_link_item(
9829 $this->apply_filters( 'support_forum_submenu', $this->get_text_inline( 'Support Forum', 'support-forum' ) ),
9830 $this->get_support_forum_url(),
9831 'wp-support-forum',
9832 null,
9833 50,
9834 $this->is_submenu_item_visible( 'support' )
9835 );
9836 }
9837 }
9838
9839 /**
9840 * @author Vova Feldman (@svovaf)
9841 * @since 1.0.1
9842 *
9843 * @param string $menu_title
9844 * @param callable $render_function
9845 * @param bool|string $page_title
9846 * @param string $capability
9847 * @param bool|string $menu_slug
9848 * @param bool|callable $before_render_function
9849 * @param int $priority
9850 * @param bool $show_submenu
9851 * @param string $class Since 1.2.1.5 can add custom classes to menu items.
9852 */
9853 function add_submenu_item(
9854 $menu_title,
9855 $render_function,
9856 $page_title = false,
9857 $capability = 'manage_options',
9858 $menu_slug = false,
9859 $before_render_function = false,
9860 $priority = WP_FS__DEFAULT_PRIORITY,
9861 $show_submenu = true,
9862 $class = ''
9863 ) {
9864 $this->_logger->entrance( 'Title = ' . $menu_title );
9865
9866 if ( $this->is_addon() ) {
9867 $parent_fs = $this->get_parent_instance();
9868
9869 if ( is_object( $parent_fs ) ) {
9870 $parent_fs->add_submenu_item(
9871 $menu_title,
9872 $render_function,
9873 $page_title,
9874 $capability,
9875 $menu_slug,
9876 $before_render_function,
9877 $priority,
9878 $show_submenu,
9879 $class
9880 );
9881
9882 return;
9883 }
9884 }
9885
9886 if ( ! isset( $this->_menu_items[ $priority ] ) ) {
9887 $this->_menu_items[ $priority ] = array();
9888 }
9889
9890 $this->_menu_items[ $priority ][] = array(
9891 'page_title' => is_string( $page_title ) ? $page_title : $menu_title,
9892 'menu_title' => $menu_title,
9893 'capability' => $capability,
9894 'menu_slug' => is_string( $menu_slug ) ? $menu_slug : strtolower( $menu_title ),
9895 'render_function' => $render_function,
9896 'before_render_function' => $before_render_function,
9897 'show_submenu' => $show_submenu,
9898 'class' => $class,
9899 );
9900 }
9901
9902 /**
9903 * @author Vova Feldman (@svovaf)
9904 * @since 1.0.1
9905 *
9906 * @param string $menu_title
9907 * @param string $url
9908 * @param bool $menu_slug
9909 * @param string $capability
9910 * @param int $priority
9911 * @param bool $show_submenu
9912 */
9913 function add_submenu_link_item(
9914 $menu_title,
9915 $url,
9916 $menu_slug = false,
9917 $capability = 'read',
9918 $priority = WP_FS__DEFAULT_PRIORITY,
9919 $show_submenu = true
9920 ) {
9921 $this->_logger->entrance( 'Title = ' . $menu_title . '; Url = ' . $url );
9922
9923 if ( $this->is_addon() ) {
9924 $parent_fs = $this->get_parent_instance();
9925
9926 if ( is_object( $parent_fs ) ) {
9927 $parent_fs->add_submenu_link_item(
9928 $menu_title,
9929 $url,
9930 $menu_slug,
9931 $capability,
9932 $priority,
9933 $show_submenu
9934 );
9935
9936 return;
9937 }
9938 }
9939
9940 if ( ! isset( $this->_menu_items[ $priority ] ) ) {
9941 $this->_menu_items[ $priority ] = array();
9942 }
9943
9944 $this->_menu_items[ $priority ][] = array(
9945 'menu_title' => $menu_title,
9946 'capability' => $capability,
9947 'menu_slug' => is_string( $menu_slug ) ? $menu_slug : strtolower( $menu_title ),
9948 'url' => $url,
9949 'page_title' => $menu_title,
9950 'render_function' => 'fs_dummy',
9951 'before_render_function' => '',
9952 'show_submenu' => $show_submenu,
9953 );
9954 }
9955
9956 #endregion ------------------------------------------------------------------
9957
9958
9959 #--------------------------------------------------------------------------------
9960 #region Actions / Hooks / Filters
9961 #--------------------------------------------------------------------------------
9962
9963 /**
9964 * @author Vova Feldman (@svovaf)
9965 * @since 1.1.7
9966 *
9967 * @param string $tag
9968 *
9969 * @return string
9970 */
9971 public function get_action_tag( $tag ) {
9972 return self::get_action_tag_static( $tag, $this->_slug, $this->is_plugin() );
9973 }
9974
9975 /**
9976 * @author Vova Feldman (@svovaf)
9977 * @since 1.2.1.6
9978 *
9979 * @param string $tag
9980 * @param string $slug
9981 * @param bool $is_plugin
9982 *
9983 * @return string
9984 */
9985 static function get_action_tag_static( $tag, $slug = '', $is_plugin = true ) {
9986 $action = "fs_{$tag}";
9987
9988 if ( ! empty( $slug ) ) {
9989 $action .= '_' . self::get_module_unique_affix( $slug, $is_plugin );
9990 }
9991
9992 return $action;
9993 }
9994
9995 /**
9996 * Returns a string that can be used to generate a unique action name,
9997 * option name, HTML element ID, or HTML element class.
9998 *
9999 * @author Leo Fajardo (@leorw)
10000 * @since 1.2.2
10001 *
10002 * @return string
10003 */
10004 public function get_unique_affix() {
10005 return self::get_module_unique_affix( $this->_slug, $this->is_plugin() );
10006 }
10007
10008 /**
10009 * Returns a string that can be used to generate a unique action name,
10010 * option name, HTML element ID, or HTML element class.
10011 *
10012 * @author Vova Feldman (@svovaf)
10013 * @since 1.2.2.5
10014 *
10015 * @param string $slug
10016 * @param bool $is_plugin
10017 *
10018 * @return string
10019 */
10020 static function get_module_unique_affix( $slug, $is_plugin = true ) {
10021 $affix = $slug;
10022
10023 if ( ! $is_plugin ) {
10024 $affix .= '-' . WP_FS__MODULE_TYPE_THEME;
10025 }
10026
10027 return $affix;
10028 }
10029
10030 /**
10031 * @author Vova Feldman (@svovaf)
10032 * @since 1.2.1
10033 * @since 1.2.2.5 The AJAX action names are based on the module ID, not like the non-AJAX actions that are
10034 * based on the slug for backward compatibility.
10035 *
10036 * @param string $tag
10037 *
10038 * @return string
10039 */
10040 function get_ajax_action( $tag ) {
10041 return self::get_ajax_action_static( $tag, $this->_module_id );
10042 }
10043
10044 /**
10045 * @author Vova Feldman (@svovaf)
10046 * @since 1.2.1.7
10047 *
10048 * @param string $tag
10049 *
10050 * @return string
10051 */
10052 function get_ajax_security( $tag ) {
10053 return wp_create_nonce( $this->get_ajax_action( $tag ) );
10054 }
10055
10056 /**
10057 * @author Vova Feldman (@svovaf)
10058 * @since 1.2.1.7
10059 *
10060 * @param string $tag
10061 */
10062 function check_ajax_referer( $tag ) {
10063 check_ajax_referer( $this->get_ajax_action( $tag ), 'security' );
10064 }
10065
10066 /**
10067 * @author Vova Feldman (@svovaf)
10068 * @since 1.2.1.6
10069 * @since 1.2.2.5 The AJAX action names are based on the module ID, not like the non-AJAX actions that are
10070 * based on the slug for backward compatibility.
10071 *
10072 * @param string $tag
10073 * @param number|null $module_id
10074 *
10075 * @return string
10076 */
10077 private static function get_ajax_action_static( $tag, $module_id = null ) {
10078 $action = "fs_{$tag}";
10079
10080 if ( ! empty( $module_id ) ) {
10081 $action .= "_{$module_id}";
10082 }
10083
10084 return $action;
10085 }
10086
10087 /**
10088 * Do action, specific for the current context plugin.
10089 *
10090 * @author Vova Feldman (@svovaf)
10091 * @since 1.0.1
10092 *
10093 * @param string $tag The name of the action to be executed.
10094 * @param mixed $arg,... Optional. Additional arguments which are passed on to the
10095 * functions hooked to the action. Default empty.
10096 *
10097 * @uses do_action()
10098 */
10099 function do_action( $tag, $arg = '' ) {
10100 $this->_logger->entrance( $tag );
10101
10102 $args = func_get_args();
10103
10104 call_user_func_array( 'do_action', array_merge(
10105 array( $this->get_action_tag( $tag ) ),
10106 array_slice( $args, 1 ) )
10107 );
10108 }
10109
10110 /**
10111 * Add action, specific for the current context plugin.
10112 *
10113 * @author Vova Feldman (@svovaf)
10114 * @since 1.0.1
10115 *
10116 * @param string $tag
10117 * @param callable $function_to_add
10118 * @param int $priority
10119 * @param int $accepted_args
10120 *
10121 * @uses add_action()
10122 */
10123 function add_action(
10124 $tag,
10125 $function_to_add,
10126 $priority = WP_FS__DEFAULT_PRIORITY,
10127 $accepted_args = 1
10128 ) {
10129 $this->_logger->entrance( $tag );
10130
10131 add_action( $this->get_action_tag( $tag ), $function_to_add, $priority, $accepted_args );
10132 }
10133
10134 /**
10135 * Add AJAX action, specific for the current context plugin.
10136 *
10137 * @author Vova Feldman (@svovaf)
10138 * @since 1.2.1
10139 *
10140 * @param string $tag
10141 * @param callable $function_to_add
10142 * @param int $priority
10143 *
10144 * @uses add_action()
10145 *
10146 * @return bool True if action added, false if no need to add the action since the AJAX call isn't matching.
10147 */
10148 function add_ajax_action(
10149 $tag,
10150 $function_to_add,
10151 $priority = WP_FS__DEFAULT_PRIORITY
10152 ) {
10153 $this->_logger->entrance( $tag );
10154
10155 return self::add_ajax_action_static(
10156 $tag,
10157 $function_to_add,
10158 $priority,
10159 $this->_module_id
10160 );
10161 }
10162
10163 /**
10164 * Add AJAX action.
10165 *
10166 * @author Vova Feldman (@svovaf)
10167 * @since 1.2.1.6
10168 *
10169 * @param string $tag
10170 * @param callable $function_to_add
10171 * @param int $priority
10172 * @param number|null $module_id
10173 *
10174 * @return bool True if action added, false if no need to add the action since the AJAX call isn't matching.
10175 * @uses add_action()
10176 *
10177 */
10178 static function add_ajax_action_static(
10179 $tag,
10180 $function_to_add,
10181 $priority = WP_FS__DEFAULT_PRIORITY,
10182 $module_id = null
10183 ) {
10184 self::$_static_logger->entrance( $tag );
10185
10186 if ( ! self::is_ajax_action_static( $tag, $module_id ) ) {
10187 return false;
10188 }
10189
10190 add_action(
10191 'wp_ajax_' . self::get_ajax_action_static( $tag, $module_id ),
10192 $function_to_add,
10193 $priority,
10194 0
10195 );
10196
10197 self::$_static_logger->info( "$tag AJAX callback action added." );
10198
10199 return true;
10200 }
10201
10202 /**
10203 * Send a JSON response back to an Ajax request.
10204 *
10205 * @author Vova Feldman (@svovaf)
10206 * @since 1.2.1.5
10207 *
10208 * @param mixed $response
10209 */
10210 static function shoot_ajax_response( $response ) {
10211 wp_send_json( $response );
10212 }
10213
10214 /**
10215 * Send a JSON response back to an Ajax request, indicating success.
10216 *
10217 * @author Vova Feldman (@svovaf)
10218 * @since 1.2.1.5
10219 *
10220 * @param mixed $data Data to encode as JSON, then print and exit.
10221 */
10222 static function shoot_ajax_success( $data = null ) {
10223 wp_send_json_success( $data );
10224 }
10225
10226 /**
10227 * Send a JSON response back to an Ajax request, indicating failure.
10228 *
10229 * @author Vova Feldman (@svovaf)
10230 * @since 1.2.1.5
10231 *
10232 * @param mixed $error Optional error message.
10233 */
10234 static function shoot_ajax_failure( $error = '' ) {
10235 $result = array( 'success' => false );
10236 if ( ! empty( $error ) ) {
10237 $result['error'] = $error;
10238 }
10239
10240 wp_send_json( $result );
10241 }
10242
10243 /**
10244 * Apply filter, specific for the current context plugin.
10245 *
10246 * @author Vova Feldman (@svovaf)
10247 * @since 1.0.9
10248 *
10249 * @param string $tag The name of the filter hook.
10250 * @param mixed $value The value on which the filters hooked to `$tag` are applied on.
10251 *
10252 * @return mixed The filtered value after all hooked functions are applied to it.
10253 *
10254 * @uses apply_filters()
10255 */
10256 function apply_filters( $tag, $value ) {
10257 $this->_logger->entrance( $tag );
10258
10259 $args = func_get_args();
10260 array_unshift( $args, $this->get_unique_affix() );
10261
10262 return call_user_func_array( 'fs_apply_filter', $args );
10263 }
10264
10265 /**
10266 * Add filter, specific for the current context plugin.
10267 *
10268 * @author Vova Feldman (@svovaf)
10269 * @since 1.0.9
10270 *
10271 * @param string $tag
10272 * @param callable $function_to_add
10273 * @param int $priority
10274 * @param int $accepted_args
10275 *
10276 * @uses add_filter()
10277 */
10278 function add_filter( $tag, $function_to_add, $priority = WP_FS__DEFAULT_PRIORITY, $accepted_args = 1 ) {
10279 $this->_logger->entrance( $tag );
10280
10281 add_filter( $this->get_action_tag( $tag ), $function_to_add, $priority, $accepted_args );
10282 }
10283
10284 /**
10285 * Check if has filter.
10286 *
10287 * @author Vova Feldman (@svovaf)
10288 * @since 1.1.4
10289 *
10290 * @param string $tag
10291 * @param callable|bool $function_to_check Optional. The callback to check for. Default false.
10292 *
10293 * @return false|int
10294 *
10295 * @uses has_filter()
10296 */
10297 function has_filter( $tag, $function_to_check = false ) {
10298 $this->_logger->entrance( $tag );
10299
10300 return has_filter( $this->get_action_tag( $tag ), $function_to_check );
10301 }
10302
10303 #endregion
10304
10305 /**
10306 * Override default i18n text phrases.
10307 *
10308 * @author Vova Feldman (@svovaf)
10309 * @since 1.1.6
10310 *
10311 * @param string[] string $key_value
10312 *
10313 * @uses fs_override_i18n()
10314 */
10315 function override_i18n( $key_value ) {
10316 fs_override_i18n( $key_value, $this->_slug );
10317 }
10318
10319 /* Account Page
10320 ------------------------------------------------------------------------------------------------------------------*/
10321 /**
10322 * Update site information.
10323 *
10324 * @author Vova Feldman (@svovaf)
10325 * @since 1.0.1
10326 *
10327 * @param bool $store Flush to Database if true.
10328 */
10329 private function _store_site( $store = true ) {
10330 $this->_logger->entrance();
10331
10332 if ( empty( $this->_site->id ) ) {
10333 $this->_logger->error( "Empty install ID, can't store site." );
10334
10335 return;
10336 }
10337
10338 $encrypted_site = clone $this->_site;
10339 $encrypted_site->plan = self::_encrypt_entity( $this->_site->plan );
10340
10341 $sites = self::get_all_sites( $this->_module_type );
10342
10343 if ( empty( $this->_storage->prev_user_id ) && $this->_user->id != $this->_site->user_id ) {
10344 /**
10345 * Store the current user ID as the previous user ID so that the previous user can be used
10346 * as the install's owner while the new owner's details are not yet available.
10347 *
10348 * This will be executed only in the `replica` site. For example, there are 2 sites, namely `original`
10349 * and `replica`, then an ownership change was initiated and completed in the `original`, the `replica`
10350 * will be using the previous user until it is updated again (e.g.: until the next clone of `original`
10351 * into `replica`.
10352 *
10353 * @author Leo Fajardo (@leorw)
10354 */
10355 $this->_storage->prev_user_id = $sites[ $this->_slug ]->user_id;
10356 }
10357
10358 $sites[ $this->_slug ] = $encrypted_site;
10359
10360 $this->set_account_option( 'sites', $sites, $store );
10361 }
10362
10363 /**
10364 * Update plugin's plans information.
10365 *
10366 * @author Vova Feldman (@svovaf)
10367 * @since 1.0.2
10368 *
10369 * @param bool $store Flush to Database if true.
10370 */
10371 private function _store_plans( $store = true ) {
10372 $this->_logger->entrance();
10373
10374 $plans = self::get_all_plans( $this->_module_type );
10375
10376 // Copy plans.
10377 $encrypted_plans = array();
10378 for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
10379 $encrypted_plans[] = self::_encrypt_entity( $this->_plans[ $i ] );
10380 }
10381
10382 $plans[ $this->_slug ] = $encrypted_plans;
10383
10384 $this->set_account_option( 'plans', $plans, $store );
10385 }
10386
10387 /**
10388 * Update user's plugin licenses.
10389 *
10390 * @author Vova Feldman (@svovaf)
10391 * @since 1.0.5
10392 *
10393 * @param bool $store
10394 * @param string|bool $plugin_slug
10395 * @param FS_Plugin_License[] $licenses
10396 */
10397 private function _store_licenses( $store = true, $plugin_slug = false, $licenses = array() ) {
10398 $this->_logger->entrance();
10399
10400 $all_licenses = self::get_all_licenses( $this->_module_type );
10401
10402 if ( ! is_string( $plugin_slug ) ) {
10403 $plugin_slug = $this->_slug;
10404 $licenses = $this->_licenses;
10405 }
10406
10407 if ( ! isset( $all_licenses[ $plugin_slug ] ) ) {
10408 $all_licenses[ $plugin_slug ] = array();
10409 }
10410
10411 $all_licenses[ $plugin_slug ][ $this->_user->id ] = $licenses;
10412
10413 $this->set_account_option( 'licenses', $all_licenses, $store );
10414 }
10415
10416 /**
10417 * Update user information.
10418 *
10419 * @author Vova Feldman (@svovaf)
10420 * @since 1.0.1
10421 *
10422 * @param bool $store Flush to Database if true.
10423 */
10424 private function _store_user( $store = true ) {
10425 $this->_logger->entrance();
10426
10427 if ( empty( $this->_user->id ) ) {
10428 $this->_logger->error( "Empty user ID, can't store user." );
10429
10430 return;
10431 }
10432
10433 $users = self::get_all_users();
10434 $users[ $this->_user->id ] = $this->_user;
10435 self::$_accounts->set_option( 'users', $users, $store );
10436 }
10437
10438 /**
10439 * Update new updates information.
10440 *
10441 * @author Vova Feldman (@svovaf)
10442 * @since 1.0.4
10443 *
10444 * @param FS_Plugin_Tag|null $update
10445 * @param bool $store Flush to Database if true.
10446 * @param bool|number $plugin_id
10447 */
10448 private function _store_update( $update, $store = true, $plugin_id = false ) {
10449 $this->_logger->entrance();
10450
10451 if ( $update instanceof FS_Plugin_Tag ) {
10452 $update->updated = time();
10453 }
10454
10455 if ( ! is_numeric( $plugin_id ) ) {
10456 $plugin_id = $this->_plugin->id;
10457 }
10458
10459 $updates = self::get_all_updates();
10460 $updates[ $plugin_id ] = $update;
10461 self::$_accounts->set_option( 'updates', $updates, $store );
10462 }
10463
10464 /**
10465 * Update new updates information.
10466 *
10467 * @author Vova Feldman (@svovaf)
10468 * @since 1.0.6
10469 *
10470 * @param FS_Plugin[] $plugin_addons
10471 * @param bool $store Flush to Database if true.
10472 */
10473 private function _store_addons( $plugin_addons, $store = true ) {
10474 $this->_logger->entrance();
10475
10476 $addons = self::get_all_addons();
10477 $addons[ $this->_plugin->id ] = $plugin_addons;
10478 self::$_accounts->set_option( 'addons', $addons, $store );
10479 }
10480
10481 /**
10482 * Delete plugin's associated add-ons.
10483 *
10484 * @author Vova Feldman (@svovaf)
10485 * @since 1.0.8
10486 *
10487 * @param bool $store
10488 *
10489 * @return bool
10490 */
10491 private function _delete_account_addons( $store = true ) {
10492 $all_addons = self::get_all_account_addons();
10493
10494 if ( ! isset( $all_addons[ $this->_plugin->id ] ) ) {
10495 return false;
10496 }
10497
10498 unset( $all_addons[ $this->_plugin->id ] );
10499
10500 self::$_accounts->set_option( 'account_addons', $all_addons, $store );
10501
10502 return true;
10503 }
10504
10505 /**
10506 * Update account add-ons list.
10507 *
10508 * @author Vova Feldman (@svovaf)
10509 * @since 1.0.6
10510 *
10511 * @param FS_Plugin[] $addons
10512 * @param bool $store Flush to Database if true.
10513 */
10514 private function _store_account_addons( $addons, $store = true ) {
10515 $this->_logger->entrance();
10516
10517 $all_addons = self::get_all_account_addons();
10518 $all_addons[ $this->_plugin->id ] = $addons;
10519 self::$_accounts->set_option( 'account_addons', $all_addons, $store );
10520 }
10521
10522 /**
10523 * Store account params in the Database.
10524 *
10525 * @author Vova Feldman (@svovaf)
10526 * @since 1.0.1
10527 */
10528 private function _store_account() {
10529 $this->_logger->entrance();
10530
10531 $this->_store_site( false );
10532 $this->_store_user( false );
10533 $this->_store_plans( false );
10534 $this->_store_licenses( false );
10535
10536 self::$_accounts->store();
10537 }
10538
10539 /**
10540 * Sync user's information.
10541 *
10542 * @author Vova Feldman (@svovaf)
10543 * @since 1.0.3
10544 * @uses FS_Api
10545 */
10546 private function _handle_account_user_sync() {
10547 $this->_logger->entrance();
10548
10549 $api = $this->get_api_user_scope();
10550
10551 // Get user's information.
10552 $user = $api->get( '/', true );
10553
10554 if ( isset( $user->id ) ) {
10555 $this->_user->first = $user->first;
10556 $this->_user->last = $user->last;
10557 $this->_user->email = $user->email;
10558
10559 $is_menu_item_account_visible = $this->is_submenu_item_visible( 'account' );
10560
10561 if ( $user->is_verified &&
10562 ( ! isset( $this->_user->is_verified ) || false === $this->_user->is_verified )
10563 ) {
10564 $this->_user->is_verified = true;
10565
10566 $this->do_action( 'account_email_verified', $user->email );
10567
10568 $this->_admin_notices->add(
10569 $this->get_text_inline( 'Your email has been successfully verified - you are AWESOME!', 'email-verified-message' ),
10570 $this->get_text_x_inline( 'Right on', 'a positive response', 'right-on' ) . '!',
10571 'success',
10572 // Make admin sticky if account menu item is invisible,
10573 // since the page will be auto redirected to the plugin's
10574 // main settings page, and the non-sticky message
10575 // will disappear.
10576 ! $is_menu_item_account_visible,
10577 false,
10578 'email_verified'
10579 );
10580 }
10581
10582 // Flush user details to DB.
10583 $this->_store_user();
10584
10585 $this->do_action( 'after_account_user_sync', $user );
10586
10587 /**
10588 * If account menu item is hidden, redirect to plugin's main settings page.
10589 *
10590 * @author Vova Feldman (@svovaf)
10591 * @since 1.1.6
10592 *
10593 * @link https://github.com/Freemius/wordpress-sdk/issues/6
10594 */
10595 if ( ! $is_menu_item_account_visible ) {
10596 fs_redirect( $this->_get_admin_page_url() );
10597 }
10598 }
10599 }
10600
10601 /**
10602 * @param bool $store
10603 *
10604 * @return FS_Plugin_Plan|object|false
10605 */
10606 private function _enrich_site_plan( $store = true ) {
10607 // Try to load plan from local cache.
10608 $plan = $this->_get_plan_by_id( $this->_site->plan->id );
10609
10610 if ( false === $plan ) {
10611 $plan = $this->_fetch_site_plan();
10612 }
10613
10614 if ( $plan instanceof FS_Plugin_Plan ) {
10615 $this->_update_plan( $plan, $store );
10616 }
10617
10618 return $plan;
10619 }
10620
10621 /**
10622 * @author Vova Feldman (@svovaf)
10623 * @since 1.0.9
10624 * @uses FS_Api
10625 *
10626 * @param bool $store
10627 *
10628 * @return FS_Plugin_Plan|object|false
10629 */
10630 private function _enrich_site_trial_plan( $store = true ) {
10631 // Try to load plan from local cache.
10632 $trial_plan = $this->_get_plan_by_id( $this->_site->trial_plan_id );
10633
10634 if ( false === $trial_plan ) {
10635 $trial_plan = $this->_fetch_site_plan( $this->_site->trial_plan_id );
10636 }
10637
10638 if ( $trial_plan instanceof FS_Plugin_Plan ) {
10639 $this->_storage->store( 'trial_plan', $trial_plan, $store );
10640 }
10641
10642 return $trial_plan;
10643 }
10644
10645 /**
10646 * @author Vova Feldman (@svovaf)
10647 * @since 1.0.9
10648 * @uses FS_Api
10649 *
10650 * @param number|bool $license_id
10651 *
10652 * @return FS_Subscription|object|bool
10653 */
10654 private function _fetch_site_license_subscription( $license_id = false ) {
10655 $this->_logger->entrance();
10656 $api = $this->get_api_site_scope();
10657
10658 if ( ! is_numeric( $license_id ) ) {
10659 $license_id = $this->_license->id;
10660 }
10661
10662 $result = $api->get( "/licenses/{$license_id}/subscriptions.json", true );
10663
10664 return ! isset( $result->error ) ?
10665 ( ( is_array( $result->subscriptions ) && 0 < count( $result->subscriptions ) ) ?
10666 new FS_Subscription( $result->subscriptions[0] ) :
10667 false
10668 ) :
10669 $result;
10670 }
10671
10672 /**
10673 * @author Vova Feldman (@svovaf)
10674 * @since 1.0.4
10675 * @uses FS_Api
10676 *
10677 * @param number|bool $plan_id
10678 *
10679 * @return FS_Plugin_Plan|object
10680 */
10681 private function _fetch_site_plan( $plan_id = false ) {
10682 $this->_logger->entrance();
10683 $api = $this->get_api_site_scope();
10684
10685 if ( ! is_numeric( $plan_id ) ) {
10686 $plan_id = $this->_site->plan->id;
10687 }
10688
10689 $plan = $api->get( "/plans/{$plan_id}.json", true );
10690
10691 return ! isset( $plan->error ) ? new FS_Plugin_Plan( $plan ) : $plan;
10692 }
10693
10694 /**
10695 * @author Vova Feldman (@svovaf)
10696 * @since 1.0.5
10697 * @uses FS_Api
10698 *
10699 * @return FS_Plugin_Plan[]|object
10700 */
10701 private function _fetch_plugin_plans() {
10702 $this->_logger->entrance();
10703 $api = $this->get_api_site_scope();
10704
10705 /**
10706 * @since 1.2.3 When running in DEV mode, retrieve pending plans as well.
10707 */
10708 $result = $api->get( '/plans.json?show_pending=' . ( $this->has_secret_key() ? 'true' : 'false' ), true );
10709
10710 if ( $this->is_api_result_object( $result, 'plans' ) && is_array( $result->plans ) ) {
10711 for ( $i = 0, $len = count( $result->plans ); $i < $len; $i ++ ) {
10712 $result->plans[ $i ] = new FS_Plugin_Plan( $result->plans[ $i ] );
10713 }
10714
10715 $result = $result->plans;
10716 }
10717
10718 return $result;
10719 }
10720
10721 /**
10722 * @author Vova Feldman (@svovaf)
10723 * @since 1.0.5
10724 * @uses FS_Api
10725 *
10726 * @param number|bool $plugin_id
10727 * @param number|bool $site_license_id
10728 *
10729 * @return FS_Plugin_License[]|object
10730 */
10731 private function _fetch_licenses( $plugin_id = false, $site_license_id = false ) {
10732 $this->_logger->entrance();
10733
10734 $api = $this->get_api_user_scope();
10735
10736 if ( ! is_numeric( $plugin_id ) ) {
10737 $plugin_id = $this->_plugin->id;
10738 }
10739
10740 $result = $api->get( "/plugins/{$plugin_id}/licenses.json", true );
10741
10742 $is_site_license_synced = false;
10743
10744 $api_errors = array();
10745
10746 if ( $this->is_api_result_object( $result, 'licenses' ) &&
10747 is_array( $result->licenses )
10748 ) {
10749 for ( $i = 0, $len = count( $result->licenses ); $i < $len; $i ++ ) {
10750 $result->licenses[ $i ] = new FS_Plugin_License( $result->licenses[ $i ] );
10751
10752 if ( ( ! $is_site_license_synced ) && is_numeric( $site_license_id ) ) {
10753 $is_site_license_synced = ( $site_license_id == $result->licenses[ $i ]->id );
10754 }
10755 }
10756
10757 $result = $result->licenses;
10758 } else {
10759 $api_errors[] = $result;
10760 $result = array();
10761 }
10762
10763 if ( ! $is_site_license_synced ) {
10764 $api = $this->get_api_site_scope();
10765
10766 if ( is_numeric( $site_license_id ) ) {
10767 // Try to retrieve a foreign license that is linked to the install.
10768 $api_result = $api->call( '/licenses.json' );
10769
10770 if ( $this->is_api_result_object( $api_result, 'licenses' ) &&
10771 is_array( $api_result->licenses )
10772 ) {
10773 $licenses = $api_result->licenses;
10774
10775 if ( ! empty( $licenses ) ) {
10776 $result[] = new FS_Plugin_License( $licenses[0] );
10777 }
10778 } else {
10779 $api_errors[] = $api_result;
10780 }
10781 } else if ( is_object( $this->_license ) ) {
10782 // Fetch foreign license by ID and license key.
10783 $license = $api->get( "/licenses/{$this->_license->id}.json?license_key=" .
10784 urlencode( $this->_license->secret_key ) );
10785
10786 if ( $this->is_api_result_entity( $license ) ) {
10787 $result[] = new FS_Plugin_License( $license );
10788 } else {
10789 $api_errors[] = $license;
10790 }
10791 }
10792 }
10793
10794 if ( is_array( $result ) && 0 < count( $result ) ) {
10795 // If found at least one license, return license collection even if there are errors.
10796 return $result;
10797 }
10798
10799 if ( ! empty( $api_errors ) ) {
10800 // If found any errors and no licenses, return first error.
10801 return $api_errors[0];
10802 }
10803
10804 // Fallback to empty licenses list.
10805 return $result;
10806 }
10807
10808 /**
10809 * @author Vova Feldman (@svovaf)
10810 * @since 1.2.0
10811 * @uses FS_Api
10812 *
10813 * @param number|bool $plugin_id
10814 * @param bool $flush
10815 *
10816 * @return FS_Payment[]|object
10817 */
10818 function _fetch_payments( $plugin_id = false, $flush = false ) {
10819 $this->_logger->entrance();
10820
10821 $api = $this->get_api_user_scope();
10822
10823 if ( ! is_numeric( $plugin_id ) ) {
10824 $plugin_id = $this->_plugin->id;
10825 }
10826
10827 $result = $api->get( "/plugins/{$plugin_id}/payments.json?include_addons=true", $flush );
10828
10829 if ( ! isset( $result->error ) ) {
10830 for ( $i = 0, $len = count( $result->payments ); $i < $len; $i ++ ) {
10831 $result->payments[ $i ] = new FS_Payment( $result->payments[ $i ] );
10832 }
10833 $result = $result->payments;
10834 }
10835
10836 return $result;
10837 }
10838
10839 /**
10840 * @author Vova Feldman (@svovaf)
10841 * @since 1.2.1.5
10842 * @uses FS_Api
10843 *
10844 * @param bool $flush
10845 *
10846 * @return \FS_Billing|mixed
10847 */
10848 function _fetch_billing( $flush = false ) {
10849 require_once WP_FS__DIR_INCLUDES . '/entities/class-fs-billing.php';
10850
10851 $billing = $this->get_api_user_scope()->get( 'billing.json', $flush );
10852
10853 if ( $this->is_api_result_entity( $billing ) ) {
10854 $billing = new FS_Billing( $billing );
10855 }
10856
10857 return $billing;
10858 }
10859
10860 /**
10861 * @author Vova Feldman (@svovaf)
10862 * @since 1.0.4
10863 *
10864 * @param FS_Plugin_Plan $plan
10865 * @param bool $store
10866 */
10867 private function _update_plan( $plan, $store = false ) {
10868 $this->_logger->entrance();
10869
10870 $this->_site->plan = $plan;
10871 $this->_store_site( $store );
10872 }
10873
10874 /**
10875 * @author Vova Feldman (@svovaf)
10876 * @since 1.0.5
10877 *
10878 * @param FS_Plugin_License[] $licenses
10879 * @param string|bool $plugin_slug
10880 */
10881 private function _update_licenses( $licenses, $plugin_slug = false ) {
10882 $this->_logger->entrance();
10883
10884 if ( is_array( $licenses ) ) {
10885 for ( $i = 0, $len = count( $licenses ); $i < $len; $i ++ ) {
10886 $licenses[ $i ]->updated = time();
10887 }
10888 }
10889
10890 if ( ! is_string( $plugin_slug ) ) {
10891 $this->_licenses = $licenses;
10892 }
10893
10894 $this->_store_licenses( true, $plugin_slug, $licenses );
10895 }
10896
10897 /**
10898 * @author Vova Feldman (@svovaf)
10899 * @since 1.0.4
10900 *
10901 * @param bool|number $plugin_id
10902 * @param bool $flush Since 1.1.7.3
10903 * @param int $expiration Since 1.2.2.7
10904 *
10905 * @return object|false New plugin tag info if exist.
10906 */
10907 private function _fetch_newer_version( $plugin_id = false, $flush = true, $expiration = WP_FS__TIME_24_HOURS_IN_SEC ) {
10908 $latest_tag = $this->_fetch_latest_version( $plugin_id, $flush, $expiration );
10909
10910 if ( ! is_object( $latest_tag ) ) {
10911 return false;
10912 }
10913
10914 // Check if version is actually newer.
10915 $has_new_version =
10916 // If it's an non-installed add-on then always return latest.
10917 ( $this->_is_addon_id( $plugin_id ) && ! $this->is_addon_activated( $plugin_id ) ) ||
10918 // Compare versions.
10919 version_compare( $this->get_plugin_version(), $latest_tag->version, '<' );
10920
10921 $this->_logger->departure( $has_new_version ? 'Found newer plugin version ' . $latest_tag->version : 'No new version' );
10922
10923 return $has_new_version ? $latest_tag : false;
10924 }
10925
10926 /**
10927 * @author Vova Feldman (@svovaf)
10928 * @since 1.0.5
10929 *
10930 * @param bool|number $plugin_id
10931 * @param bool $flush Since 1.1.7.3
10932 * @param int $expiration Since 1.2.2.7
10933 *
10934 * @return bool|FS_Plugin_Tag
10935 */
10936 function get_update( $plugin_id = false, $flush = true, $expiration = WP_FS__TIME_24_HOURS_IN_SEC ) {
10937 $this->_logger->entrance();
10938
10939 if ( ! is_numeric( $plugin_id ) ) {
10940 $plugin_id = $this->_plugin->id;
10941 }
10942
10943 $this->check_updates( true, $plugin_id, $flush, $expiration );
10944 $updates = $this->get_all_updates();
10945
10946 return isset( $updates[ $plugin_id ] ) && is_object( $updates[ $plugin_id ] ) ? $updates[ $plugin_id ] : false;
10947 }
10948
10949 /**
10950 * Check if site assigned with active license.
10951 *
10952 * @author Vova Feldman (@svovaf)
10953 * @since 1.0.6
10954 *
10955 * @deprecated Please use has_active_valid_license() instead because license can be cancelled.
10956 */
10957 function has_active_license() {
10958 return (
10959 is_object( $this->_license ) &&
10960 is_numeric( $this->_license->id ) &&
10961 ! $this->_license->is_expired()
10962 );
10963 }
10964
10965 /**
10966 * Check if site assigned with active & valid (not expired) license.
10967 *
10968 * @author Vova Feldman (@svovaf)
10969 * @since 1.2.1
10970 */
10971 function has_active_valid_license() {
10972 return (
10973 is_object( $this->_license ) &&
10974 is_numeric( $this->_license->id ) &&
10975 $this->_license->is_active() &&
10976 $this->_license->is_valid()
10977 );
10978 }
10979
10980 /**
10981 * Check if site assigned with license with enabled features.
10982 *
10983 * @author Vova Feldman (@svovaf)
10984 * @since 1.0.6
10985 *
10986 * @return bool
10987 */
10988 function has_features_enabled_license() {
10989 return (
10990 is_object( $this->_license ) &&
10991 is_numeric( $this->_license->id ) &&
10992 $this->_license->is_features_enabled()
10993 );
10994 }
10995
10996 /**
10997 * Check if user is a trial or have feature enabled license.
10998 *
10999 * @author Vova Feldman (@svovaf)
11000 * @since 1.1.7
11001 *
11002 * @return bool
11003 */
11004 function can_use_premium_code() {
11005 return $this->is_trial() || $this->has_features_enabled_license();
11006 }
11007
11008 /**
11009 * Checks if the current user can activate plugins or switch themes. Note that this method should only be used
11010 * after the `init` action is triggered because it is using `current_user_can()` which is only functional after
11011 * the context user is authenticated.
11012 *
11013 * @author Leo Fajardo (@leorw)
11014 * @since 1.2.2
11015 *
11016 * @return bool
11017 */
11018 function is_user_admin() {
11019 return ( $this->is_plugin() && current_user_can( 'activate_plugins' ) )
11020 || ( $this->is_theme() && current_user_can( 'switch_themes' ) );
11021 }
11022
11023 /**
11024 * Sync site's plan.
11025 *
11026 * @author Vova Feldman (@svovaf)
11027 * @since 1.0.3
11028 *
11029 * @uses FS_Api
11030 *
11031 * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by
11032 * the admin.
11033 */
11034 private function _sync_license( $background = false ) {
11035 $this->_logger->entrance();
11036
11037 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
11038
11039 $is_addon_sync = ( ! $this->_plugin->is_addon() && $plugin_id != $this->get_id() );
11040
11041 if ( $is_addon_sync ) {
11042 $this->_sync_addon_license( $plugin_id, $background );
11043 } else {
11044 $this->_sync_plugin_license( $background );
11045 }
11046
11047 $this->do_action( 'after_account_plan_sync', $this->_site->plan->name );
11048 }
11049
11050 /**
11051 * Sync plugin's add-on license.
11052 *
11053 * @author Vova Feldman (@svovaf)
11054 * @since 1.0.6
11055 * @uses FS_Api
11056 *
11057 * @param number $addon_id
11058 * @param bool $background
11059 */
11060 private function _sync_addon_license( $addon_id, $background ) {
11061 $this->_logger->entrance();
11062
11063 if ( $this->is_addon_activated( $addon_id ) ) {
11064 // If already installed, use add-on sync.
11065 $fs_addon = self::get_instance_by_id( $addon_id );
11066 $fs_addon->_sync_license( $background );
11067
11068 return;
11069 }
11070
11071 // Validate add-on exists.
11072 $addon = $this->get_addon( $addon_id );
11073
11074 if ( ! is_object( $addon ) ) {
11075 return;
11076 }
11077
11078 // Add add-on into account add-ons.
11079 $account_addons = $this->get_account_addons();
11080 if ( ! is_array( $account_addons ) ) {
11081 $account_addons = array();
11082 }
11083 $account_addons[] = $addon->id;
11084 $account_addons = array_unique( $account_addons );
11085 $this->_store_account_addons( $account_addons );
11086
11087 // Load add-on licenses.
11088 $licenses = $this->_fetch_licenses( $addon->id );
11089
11090 // Sync add-on licenses.
11091 if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) {
11092 $this->_update_licenses( $licenses, $addon->slug );
11093
11094 if ( ! $this->is_addon_installed( $addon->id ) && FS_License_Manager::has_premium_license( $licenses ) ) {
11095 $plans_result = $this->get_api_site_or_plugin_scope()->get( "/addons/{$addon_id}/plans.json" );
11096
11097 if ( ! isset( $plans_result->error ) ) {
11098 $plans = array();
11099 foreach ( $plans_result->plans as $plan ) {
11100 $plans[] = new FS_Plugin_Plan( $plan );
11101 }
11102
11103 $this->_admin_notices->add_sticky(
11104 sprintf(
11105 ( FS_Plan_Manager::instance()->has_free_plan( $plans ) ?
11106 $this->get_text_inline( 'Your %s Add-on plan was successfully upgraded.', 'addon-successfully-upgraded-message' ) :
11107 /* translators: %s:product name, e.g. Facebook add-on was successfully... */
11108 $this->get_text_inline( '%s Add-on was successfully purchased.', 'addon-successfully-purchased-message' ) ),
11109 $addon->title
11110 ) . ' ' . $this->get_latest_download_link(
11111 $this->get_text_inline( 'Download the latest version', 'download-latest-version' ),
11112 $addon_id
11113 ),
11114 'addon_plan_upgraded_' . $addon->slug,
11115 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
11116 );
11117 }
11118 }
11119 }
11120 }
11121
11122 /**
11123 * Sync site's plugin plan.
11124 *
11125 * @author Vova Feldman (@svovaf)
11126 * @since 1.0.6
11127 * @uses FS_Api
11128 *
11129 * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by
11130 * the admin.
11131 */
11132 private function _sync_plugin_license( $background = false ) {
11133 $this->_logger->entrance();
11134
11135 /**
11136 * Sync site info.
11137 *
11138 * @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.
11139 */
11140 $site = $this->send_install_update( array(), true );
11141
11142 $plan_change = 'none';
11143
11144 if ( ! $this->is_api_result_entity( $site ) ) {
11145 // Show API messages only if not background sync or if paying customer.
11146 if ( ! $background || $this->is_paying() ) {
11147 // Try to ping API to see if not blocked.
11148 if ( ! FS_Api::test() ) {
11149 /**
11150 * Failed to ping API - blocked!
11151 *
11152 * @author Vova Feldman (@svovaf)
11153 * @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.
11154 */
11155 $api = $this->get_api_site_scope();
11156
11157 if ( ! self::$_global_admin_notices->has_sticky( 'api_blocked' ) ) {
11158 self::$_global_admin_notices->add(
11159 sprintf(
11160 $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' ),
11161 $this->get_plugin_name(),
11162 '<a href="' . $api->get_url() . '" target="_blank">' . $api->get_url() . '</a>'
11163 ) . '<br> ' . $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . var_export( $site->error, true ),
11164 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
11165 'error',
11166 $background,
11167 false,
11168 'api_blocked'
11169 );
11170 }
11171 } else {
11172 // Authentication params are broken.
11173 $this->_admin_notices->add(
11174 $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' ),
11175 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
11176 'error'
11177 );
11178 }
11179 }
11180
11181 // No reason to continue with license sync while there are API issues.
11182 return;
11183 }
11184
11185 // Remove sticky API connectivity message.
11186 self::$_global_admin_notices->remove_sticky( 'api_blocked' );
11187
11188 $site = new FS_Site( $site );
11189
11190 // Sync plans.
11191 $this->_sync_plans();
11192
11193 if ( ! $this->has_paid_plan() ) {
11194 $this->_site = $site;
11195 $this->_enrich_site_plan( true );
11196 $this->_store_site();
11197 } else {
11198 /**
11199 * Sync licenses. Pass the site's license ID so that the foreign licenses will be fetched if the license
11200 * associated with that ID is not included in the user's licenses collection.
11201 */
11202 $this->_sync_licenses( $site->license_id );
11203
11204 // Check if plan / license changed.
11205 if ( ! FS_Entity::equals( $site->plan, $this->_site->plan ) ||
11206 // Check if trial started.
11207 $site->trial_plan_id != $this->_site->trial_plan_id ||
11208 $site->trial_ends != $this->_site->trial_ends ||
11209 // Check if license changed.
11210 $site->license_id != $this->_site->license_id
11211 ) {
11212 if ( $site->is_trial() && ( ! $this->_site->is_trial() || $site->trial_ends != $this->_site->trial_ends ) ) {
11213 // New trial started.
11214 $this->_site = $site;
11215 $plan_change = 'trial_started';
11216
11217 // Store trial plan information.
11218 $this->_enrich_site_trial_plan( true );
11219
11220 // For trial with subscription use-case.
11221 $new_license = is_null( $site->license_id ) ? null : $this->_get_license_by_id( $site->license_id );
11222
11223 if ( is_object( $new_license ) && $new_license->is_valid() ) {
11224 $this->_site = $site;
11225 $this->_update_site_license( $new_license );
11226 $this->_store_licenses();
11227 $this->_enrich_site_plan( true );
11228
11229 $this->_sync_site_subscription( $this->_license );
11230 }
11231 } else if ( $this->_site->is_trial() && ! $site->is_trial() && ! is_numeric( $site->license_id ) ) {
11232 // Was in trial, but now trial expired and no license ID.
11233 // New trial started.
11234 $this->_site = $site;
11235 $plan_change = 'trial_expired';
11236
11237 // Clear trial plan information.
11238 $this->_storage->trial_plan = null;
11239
11240 } else {
11241 $is_free = $this->is_free_plan();
11242
11243 // Make sure license exist and not expired.
11244 $new_license = is_null( $site->license_id ) ?
11245 null :
11246 $this->_get_license_by_id( $site->license_id );
11247
11248 if ( $is_free && is_null( $new_license ) && $this->has_any_license() && $this->_license->is_cancelled ) {
11249 // License cancelled.
11250 $this->_site = $site;
11251 $this->_update_site_license( $new_license );
11252 $this->_store_licenses();
11253 $this->_enrich_site_plan( true );
11254
11255 $plan_change = 'cancelled';
11256 } else if ( $is_free && ( ( ! is_object( $new_license ) || $new_license->is_expired() ) ) ) {
11257 // The license is expired, so ignore upgrade method.
11258 } else {
11259 // License changed.
11260 $this->_site = $site;
11261 $this->_update_site_license( $new_license );
11262 $this->_store_licenses();
11263 $this->_enrich_site_plan( true );
11264
11265 $plan_change = $is_free ?
11266 'upgraded' :
11267 ( is_object( $new_license ) ?
11268 'changed' :
11269 'downgraded' );
11270 }
11271 }
11272
11273 // Store updated site info.
11274 $this->_store_site();
11275 } else {
11276 if ( is_object( $this->_license ) && $this->_license->is_expired() ) {
11277 if ( ! $this->has_features_enabled_license() ) {
11278 $this->_deactivate_license();
11279 $plan_change = 'downgraded';
11280 } else {
11281 $plan_change = 'expired';
11282 }
11283 }
11284
11285 if ( is_numeric( $site->license_id ) && is_object( $this->_license ) ) {
11286 $this->_sync_site_subscription( $this->_license );
11287 }
11288 }
11289 }
11290
11291 $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...';
11292
11293 if ( $this->has_paid_plan() ) {
11294 switch ( $plan_change ) {
11295 case 'none':
11296 if ( ! $background && is_admin() ) {
11297 $plan = $this->is_trial() ?
11298 $this->_storage->trial_plan :
11299 $this->_site->plan;
11300
11301 if ( $plan->is_free() ) {
11302 $this->_admin_notices->add(
11303 sprintf(
11304 $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' ),
11305 '<i><b>' . $plan->title . ( $this->is_trial() ? ' ' . $this->get_text_x_inline( 'Trial', 'trial period', 'trial' ) : '' ) . '</b></i>'
11306 ) . ' ' . sprintf(
11307 '<a href="%s">%s</a>',
11308 $this->contact_url(
11309 'bug',
11310 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' ),
11311 strtoupper( $plan->name )
11312 )
11313 ),
11314 $this->get_text_inline( 'Please contact us here', 'contact-us-here' )
11315 ),
11316 $hmm_text
11317 );
11318 }
11319 }
11320 break;
11321 case 'upgraded':
11322 $this->_admin_notices->add_sticky(
11323 sprintf(
11324 $this->get_text_inline( 'Your plan was successfully upgraded.', 'plan-upgraded-message' ),
11325 '<i>' . $this->get_plugin_name() . '</i>'
11326 ) . $this->get_complete_upgrade_instructions(),
11327 'plan_upgraded',
11328 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
11329 );
11330
11331 $this->_admin_notices->remove_sticky( array(
11332 'trial_started',
11333 'trial_promotion',
11334 'trial_expired',
11335 'activation_complete',
11336 ) );
11337 break;
11338 case 'changed':
11339 $this->_admin_notices->add_sticky(
11340 sprintf(
11341 $this->get_text_inline( 'Your plan was successfully changed to %s.', 'plan-changed-to-x-message' ),
11342 $this->_site->plan->title
11343 ),
11344 'plan_changed'
11345 );
11346
11347 $this->_admin_notices->remove_sticky( array(
11348 'trial_started',
11349 'trial_promotion',
11350 'trial_expired',
11351 'activation_complete',
11352 ) );
11353 break;
11354 case 'downgraded':
11355 $this->_admin_notices->add_sticky(
11356 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 ),
11357 'license_expired',
11358 $hmm_text
11359 );
11360 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
11361 break;
11362 case 'cancelled':
11363 $this->_admin_notices->add(
11364 $this->get_text_inline( 'Your license has been cancelled. If you think it\'s a mistake, please contact support.', 'license-cancelled' ) . ' ' .
11365 sprintf(
11366 '<a href="%s">%s</a>',
11367 $this->contact_url( 'bug' ),
11368 $this->get_text_inline( 'Please contact us here', 'contact-us-here' )
11369 ),
11370 $hmm_text,
11371 'error'
11372 );
11373 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
11374 break;
11375 case 'expired':
11376 $this->_admin_notices->add_sticky(
11377 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->_site->plan->title ),
11378 'license_expired',
11379 $hmm_text
11380 );
11381 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
11382 break;
11383 case 'trial_started':
11384 $this->_admin_notices->add_sticky(
11385 sprintf(
11386 $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ),
11387 '<i>' . $this->get_plugin_name() . '</i>'
11388 ) . $this->get_complete_upgrade_instructions( $this->_storage->trial_plan->title ),
11389 'trial_started',
11390 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
11391 );
11392
11393 $this->_admin_notices->remove_sticky( array(
11394 'trial_promotion',
11395 ) );
11396 break;
11397 case 'trial_expired':
11398 $this->_admin_notices->add_sticky(
11399 $this->get_text_inline( 'Your trial has expired. You can still continue using all our free features.', 'trial-expired-message' ),
11400 'trial_expired',
11401 $hmm_text
11402 );
11403 $this->_admin_notices->remove_sticky( array(
11404 'trial_started',
11405 'trial_promotion',
11406 'plan_upgraded',
11407 ) );
11408 break;
11409 }
11410 }
11411
11412 if ( 'none' !== $plan_change ) {
11413 $this->do_action( 'after_license_change', $plan_change, $this->_site->plan );
11414 }
11415 }
11416
11417 /**
11418 * @author Vova Feldman (@svovaf)
11419 * @since 1.0.5
11420 *
11421 * @param bool $background
11422 */
11423 protected function _activate_license( $background = false ) {
11424 $this->_logger->entrance();
11425
11426 $license_id = fs_request_get( 'license_id' );
11427
11428 if ( FS_Plugin_License::is_valid_id( $license_id ) && $license_id == $this->_site->license_id ) {
11429 // License is already activated.
11430 return;
11431 }
11432
11433 $premium_license = FS_Plugin_License::is_valid_id( $license_id ) ?
11434 $this->_get_license_by_id( $license_id ) :
11435 $this->_get_available_premium_license();
11436
11437 if ( ! is_object( $premium_license ) ) {
11438 return;
11439 }
11440
11441 /**
11442 * If the premium license is already associated with the install, just
11443 * update the license reference (activation is not required).
11444 *
11445 * @since 1.1.9
11446 */
11447 if ( $premium_license->id == $this->_site->license_id ) {
11448 // License is already activated.
11449 $this->_update_site_license( $premium_license );
11450 $this->_enrich_site_plan( false );
11451 $this->_store_account();
11452
11453 return;
11454 }
11455
11456 if ( $this->_site->user_id != $premium_license->user_id ) {
11457 $api_request_params = array( 'license_key' => $premium_license->secret_key );
11458 } else {
11459 $api_request_params = array();
11460 }
11461
11462 $api = $this->get_api_site_scope();
11463 $license = $api->call( "/licenses/{$premium_license->id}.json", 'put', $api_request_params );
11464
11465 if ( ! $this->is_api_result_entity( $license ) ) {
11466 if ( ! $background ) {
11467 $this->_admin_notices->add( sprintf(
11468 '%s %s',
11469 $this->get_text_inline( 'It looks like the license could not be activated.', 'license-activation-failed-message' ),
11470 ( is_object( $license ) && isset( $license->error ) ?
11471 $license->error->message :
11472 sprintf( '%s<br><code>%s</code>',
11473 $this->get_text_inline( 'Error received from the server:', 'server-error-message' ),
11474 var_export( $license, true )
11475 )
11476 )
11477 ),
11478 $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...',
11479 'error'
11480 );
11481 }
11482
11483 return;
11484 }
11485 $premium_license = new FS_Plugin_License( $license );
11486
11487 // Updated site plan.
11488 $site = $this->get_api_site_scope()->get( '/', true );
11489 if ( $this->is_api_result_entity( $site ) ) {
11490 $this->_site = new FS_Site( $site );
11491 }
11492 $this->_update_site_license( $premium_license );
11493 $this->_enrich_site_plan( false );
11494
11495 $this->_store_account();
11496
11497 if ( ! $background ) {
11498 $this->_admin_notices->add_sticky(
11499 $this->get_text_inline( 'Your license was successfully activated.', 'license-activated-message' ) .
11500 $this->get_complete_upgrade_instructions(),
11501 'license_activated',
11502 $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
11503 );
11504 }
11505
11506 $this->_admin_notices->remove_sticky( array(
11507 'trial_promotion',
11508 'license_expired',
11509 ) );
11510 }
11511
11512 /**
11513 * @author Vova Feldman (@svovaf)
11514 * @since 1.0.5
11515 *
11516 * @param bool $show_notice
11517 */
11518 protected function _deactivate_license( $show_notice = true ) {
11519 $this->_logger->entrance();
11520
11521 $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...';
11522
11523 if ( ! is_object( $this->_license ) ) {
11524 $this->_admin_notices->add(
11525 sprintf( $this->get_text_inline( 'It looks like your site currently doesn\'t have an active license.', 'no-active-license-message' ), $this->_site->plan->title ),
11526 $hmm_text
11527 );
11528
11529 return;
11530 }
11531
11532 $api = $this->get_api_site_scope();
11533 $license = $api->call( "/licenses/{$this->_site->license_id}.json", 'delete' );
11534
11535 if ( isset( $license->error ) ) {
11536 $this->_admin_notices->add(
11537 $this->get_text_inline( 'It looks like the license deactivation failed.', 'license-deactivation-failed-message' ) . '<br> ' .
11538 $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . ' ' . var_export( $license->error, true ),
11539 $hmm_text,
11540 'error'
11541 );
11542
11543 return;
11544 }
11545
11546 // Update license cache.
11547 for ( $i = 0, $len = count( $this->_licenses ); $i < $len; $i ++ ) {
11548 if ( $license->id == $this->_licenses[ $i ]->id ) {
11549 $this->_licenses[ $i ] = new FS_Plugin_License( $license );
11550 }
11551 }
11552
11553 // Updated site plan to default.
11554 $this->_sync_plans();
11555 $this->_site->plan->id = $this->_plans[0]->id;
11556 // Unlink license from site.
11557 $this->_update_site_license( null );
11558 $this->_enrich_site_plan( false );
11559
11560 $this->_store_account();
11561
11562 if ( $show_notice ) {
11563 $this->_admin_notices->add(
11564 sprintf( $this->get_text_inline( 'Your license was successfully deactivated, you are back to the %s plan.', 'license-deactivation-message' ), $this->_site->plan->title ),
11565 $this->get_text_inline( 'O.K', 'ok' )
11566 );
11567 }
11568
11569 $this->_admin_notices->remove_sticky( array(
11570 'plan_upgraded',
11571 'license_activated',
11572 ) );
11573 }
11574
11575 /**
11576 * Site plan downgrade.
11577 *
11578 * @author Vova Feldman (@svovaf)
11579 * @since 1.0.4
11580 *
11581 * @uses FS_Api
11582 */
11583 private function _downgrade_site() {
11584 $this->_logger->entrance();
11585
11586 $api = $this->get_api_site_scope();
11587 $site = $api->call( 'downgrade.json', 'put' );
11588
11589 $plan_downgraded = false;
11590 $plan = false;
11591 if ( $this->is_api_result_entity( $site ) ) {
11592 $prev_plan_id = $this->_site->plan->id;
11593
11594 // Update new site plan id.
11595 $this->_site->plan->id = $site->plan_id;
11596
11597 $plan = $this->_enrich_site_plan();
11598 $subscription = $this->_sync_site_subscription( $this->_license );
11599
11600 // Plan downgraded if plan was changed or subscription was cancelled.
11601 $plan_downgraded = ( $plan instanceof FS_Plugin_Plan && $prev_plan_id != $plan->id ) ||
11602 ( is_object( $subscription ) && ! isset( $subscription->error ) && ! $subscription->is_active() );
11603 } else {
11604 // handle different error cases.
11605
11606 }
11607
11608 if ( $plan_downgraded ) {
11609 // Remove previous sticky message about upgrade (if exist).
11610 $this->_admin_notices->remove_sticky( 'plan_upgraded' );
11611
11612 $this->_admin_notices->add(
11613 sprintf( $this->get_text_inline( 'Your plan was successfully downgraded. Your %s plan license will expire in %s.', 'plan-x-downgraded-message' ),
11614 $plan->title,
11615 human_time_diff( time(), strtotime( $this->_license->expiration ) )
11616 )
11617 );
11618
11619 // Store site updates.
11620 $this->_store_site();
11621 } else {
11622 $this->_admin_notices->add(
11623 $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' ),
11624 $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...',
11625 'error'
11626 );
11627 }
11628 }
11629
11630 /**
11631 * @author Vova Feldman (@svovaf)
11632 * @since 1.1.8.1
11633 *
11634 * @param bool|string $plan_name
11635 *
11636 * @return bool If trial was successfully started.
11637 */
11638 function start_trial( $plan_name = false ) {
11639 $this->_logger->entrance();
11640
11641 // Alias.
11642 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
11643
11644 if ( $this->is_trial() ) {
11645 // Already in trial mode.
11646 $this->_admin_notices->add(
11647 sprintf( $this->get_text_inline( 'You are already running the %s in a trial mode.', 'in-trial-mode' ), $this->_module_type ),
11648 $oops_text,
11649 'error'
11650 );
11651
11652 return false;
11653 }
11654
11655 if ( $this->_site->is_trial_utilized() ) {
11656 // Trial was already utilized.
11657 $this->_admin_notices->add(
11658 $this->get_text_inline( 'You already utilized a trial before.', 'trial-utilized' ),
11659 $oops_text,
11660 'error'
11661 );
11662
11663 return false;
11664 }
11665
11666 if ( false !== $plan_name ) {
11667 $plan = $this->get_plan_by_name( $plan_name );
11668
11669 if ( false === $plan ) {
11670 // Plan doesn't exist.
11671 $this->_admin_notices->add(
11672 sprintf( $this->get_text_inline( 'Plan %s do not exist, therefore, can\'t start a trial.', 'trial-plan-x-not-exist' ), $plan_name ),
11673 $oops_text,
11674 'error'
11675 );
11676
11677 return false;
11678 }
11679
11680 if ( ! $plan->has_trial() ) {
11681 // Plan doesn't exist.
11682 $this->_admin_notices->add(
11683 sprintf( $this->get_text_inline( 'Plan %s does not support a trial period.', 'plan-x-no-trial' ), $plan_name ),
11684 $oops_text,
11685 'error'
11686 );
11687
11688 return false;
11689 }
11690 } else {
11691 if ( ! $this->has_trial_plan() ) {
11692 // None of the plans have a trial.
11693 $this->_admin_notices->add(
11694 sprintf( $this->get_text_inline( 'None of the %s\'s plans supports a trial period.', 'no-trials' ), $this->_module_type ),
11695 $oops_text,
11696 'error'
11697 );
11698
11699 return false;
11700 }
11701
11702 $plans_with_trial = FS_Plan_Manager::instance()->get_trial_plans( $this->_plans );
11703
11704 $plan = $plans_with_trial[0];
11705 }
11706
11707 $api = $this->get_api_site_scope();
11708 $plan = $api->call( "plans/{$plan->id}/trials.json", 'post' );
11709
11710 if ( ! $this->is_api_result_entity( $plan ) ) {
11711 // Some API error while trying to start the trial.
11712 $this->_admin_notices->add(
11713 sprintf( $this->get_text_inline( 'Unexpected API error. Please contact the %s\'s author with the following error.', 'unexpected-api-error' ), $this->_module_type )
11714 . ' ' . var_export( $plan, true ),
11715 $oops_text,
11716 'error'
11717 );
11718
11719 return false;
11720 }
11721
11722 // Sync license.
11723 $this->_sync_license();
11724
11725 return $this->is_trial();
11726 }
11727
11728 /**
11729 * Cancel site trial.
11730 *
11731 * @author Vova Feldman (@svovaf)
11732 * @since 1.0.9
11733 *
11734 * @uses FS_Api
11735 */
11736 private function _cancel_trial() {
11737 $this->_logger->entrance();
11738
11739 // Alias.
11740 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
11741
11742 if ( ! $this->is_trial() ) {
11743 $this->_admin_notices->add(
11744 $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' ),
11745 $oops_text,
11746 'error'
11747 );
11748
11749 return;
11750 }
11751
11752 $api = $this->get_api_site_scope();
11753 $site = $api->call( 'trials.json', 'delete' );
11754
11755 $trial_cancelled = false;
11756
11757 if ( $this->is_api_result_entity( $site ) ) {
11758 $prev_trial_ends = $this->_site->trial_ends;
11759
11760 if ( $this->is_paid_trial() ) {
11761 $this->_license->expiration = $site->trial_ends;
11762 $this->_license->is_cancelled = true;
11763 $this->_update_site_license( $this->_license );
11764 $this->_store_licenses();
11765
11766 // Clear subscription reference.
11767 $this->_sync_site_subscription( null );
11768 }
11769
11770 // Update site info.
11771 $this->_site = new FS_Site( $site );
11772 $this->_enrich_site_plan();
11773
11774 $trial_cancelled = ( $prev_trial_ends != $site->trial_ends );
11775 } else {
11776 // handle different error cases.
11777
11778 }
11779
11780 if ( $trial_cancelled ) {
11781 // Remove previous sticky messages about upgrade or trial (if exist).
11782 $this->_admin_notices->remove_sticky( array(
11783 'trial_started',
11784 'trial_promotion',
11785 'plan_upgraded',
11786 ) );
11787
11788 // Store site updates.
11789 $this->_store_site();
11790
11791 if ( ! $this->is_addon() ||
11792 ! $this->deactivate_premium_only_addon_without_license( true )
11793 ) {
11794 $this->_admin_notices->add(
11795 sprintf( $this->get_text_inline( 'Your %s free trial was successfully cancelled.', 'trial-cancel-message' ), $this->_storage->trial_plan->title )
11796 );
11797 }
11798
11799 // Clear trial plan information.
11800 unset( $this->_storage->trial_plan );
11801 } else {
11802 $this->_admin_notices->add(
11803 $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' ),
11804 $oops_text,
11805 'error'
11806 );
11807 }
11808 }
11809
11810 /**
11811 * @author Vova Feldman (@svovaf)
11812 * @since 1.0.6
11813 *
11814 * @param bool|number $plugin_id
11815 *
11816 * @return bool
11817 */
11818 private function _is_addon_id( $plugin_id ) {
11819 return is_numeric( $plugin_id ) && ( $this->get_id() != $plugin_id );
11820 }
11821
11822 /**
11823 * Check if user eligible to download premium version updates.
11824 *
11825 * @author Vova Feldman (@svovaf)
11826 * @since 1.0.6
11827 *
11828 * @return bool
11829 */
11830 private function _can_download_premium() {
11831 return $this->has_active_valid_license() ||
11832 ( $this->is_trial() && ! $this->get_trial_plan()->is_free() );
11833 }
11834
11835 /**
11836 *
11837 * @author Vova Feldman (@svovaf)
11838 * @since 1.0.6
11839 *
11840 * @param bool|number $addon_id
11841 * @param string $type "json" or "zip"
11842 *
11843 * @return string
11844 */
11845 private function _get_latest_version_endpoint( $addon_id = false, $type = 'json' ) {
11846
11847 $is_addon = $this->_is_addon_id( $addon_id );
11848
11849 $is_premium = null;
11850 if ( ! $is_addon ) {
11851 $is_premium = $this->_can_download_premium();
11852 } else if ( $this->is_addon_activated( $addon_id ) ) {
11853 $is_premium = self::get_instance_by_id( $addon_id )->_can_download_premium();
11854 }
11855
11856 // If add-on, then append add-on ID.
11857 $endpoint = ( $is_addon ? "/addons/$addon_id" : '' ) .
11858 '/updates/latest.' . $type;
11859
11860 // If add-on and not yet activated, try to fetch based on server licensing.
11861 if ( is_bool( $is_premium ) ) {
11862 $endpoint = add_query_arg( 'is_premium', json_encode( $is_premium ), $endpoint );
11863 }
11864
11865 if ( $this->has_secret_key() ) {
11866 $endpoint = add_query_arg( 'type', 'all', $endpoint );
11867 }
11868
11869 return $endpoint;
11870 }
11871
11872 /**
11873 * @author Vova Feldman (@svovaf)
11874 * @since 1.0.4
11875 *
11876 * @param bool|number $addon_id
11877 * @param bool $flush Since 1.1.7.3
11878 * @param int $expiration Since 1.2.2.7
11879 *
11880 * @return object|false Plugin latest tag info.
11881 */
11882 function _fetch_latest_version( $addon_id = false, $flush = true, $expiration = WP_FS__TIME_24_HOURS_IN_SEC ) {
11883 $this->_logger->entrance();
11884
11885 /**
11886 * @since 1.1.7.3 Check for plugin updates from Freemius only if opted-in.
11887 * @since 1.1.7.4 Also check updates for add-ons.
11888 */
11889 if ( ! $this->is_registered() &&
11890 ! $this->_is_addon_id( $addon_id )
11891 ) {
11892 return false;
11893 }
11894
11895 $tag = $this->get_api_site_or_plugin_scope()->get(
11896 $this->_get_latest_version_endpoint( $addon_id, 'json' ),
11897 $flush,
11898 $expiration
11899 );
11900
11901 $latest_version = ( is_object( $tag ) && isset( $tag->version ) ) ? $tag->version : 'couldn\'t get';
11902
11903 $this->_logger->departure( 'Latest version ' . $latest_version );
11904
11905 return ( is_object( $tag ) && isset( $tag->version ) ) ? $tag : false;
11906 }
11907
11908 #----------------------------------------------------------------------------------
11909 #region Download Plugin
11910 #----------------------------------------------------------------------------------
11911
11912 /**
11913 * Download latest plugin version, based on plan.
11914 *
11915 * Not like _download_latest(), this will redirect the page
11916 * to secure download url to prevent dual download (from FS to WP server,
11917 * and then from WP server to the client / browser).
11918 *
11919 * @author Vova Feldman (@svovaf)
11920 * @since 1.0.9
11921 *
11922 * @param bool|number $plugin_id
11923 *
11924 * @uses FS_Api
11925 * @uses wp_redirect()
11926 */
11927 private function download_latest_directly( $plugin_id = false ) {
11928 $this->_logger->entrance();
11929
11930 wp_redirect( $this->get_latest_download_api_url( $plugin_id ) );
11931 }
11932
11933 /**
11934 * Get latest plugin FS API download URL.
11935 *
11936 * @author Vova Feldman (@svovaf)
11937 * @since 1.0.9
11938 *
11939 * @param bool|number $plugin_id
11940 *
11941 * @return string
11942 */
11943 private function get_latest_download_api_url( $plugin_id = false ) {
11944 $this->_logger->entrance();
11945
11946 return $this->get_api_site_scope()->get_signed_url(
11947 $this->_get_latest_version_endpoint( $plugin_id, 'zip' )
11948 );
11949 }
11950
11951 /**
11952 * Get payment invoice URL.
11953 *
11954 * @author Vova Feldman (@svovaf)
11955 * @since 1.2.0
11956 *
11957 * @param bool|number $payment_id
11958 *
11959 * @return string
11960 */
11961 function _get_invoice_api_url( $payment_id = false ) {
11962 $this->_logger->entrance();
11963
11964 return $this->get_api_user_scope()->get_signed_url(
11965 "/payments/{$payment_id}/invoice.pdf"
11966 );
11967 }
11968
11969 /**
11970 * Get latest plugin download link.
11971 *
11972 * @author Vova Feldman (@svovaf)
11973 * @since 1.0.9
11974 *
11975 * @param string $label
11976 * @param bool|number $plugin_id
11977 *
11978 * @return string
11979 */
11980 private function get_latest_download_link( $label, $plugin_id = false ) {
11981 return sprintf(
11982 '<a target="_blank" href="%s">%s</a>',
11983 $this->_get_latest_download_local_url( $plugin_id ),
11984 $label
11985 );
11986 }
11987
11988 /**
11989 * Get latest plugin download local URL.
11990 *
11991 * @author Vova Feldman (@svovaf)
11992 * @since 1.0.9
11993 *
11994 * @param bool|number $plugin_id
11995 *
11996 * @return string
11997 */
11998 function _get_latest_download_local_url( $plugin_id = false ) {
11999 // Add timestamp to protect from caching.
12000 $params = array( 'ts' => WP_FS__SCRIPT_START_TIME );
12001
12002 if ( ! empty( $plugin_id ) ) {
12003 $params['plugin_id'] = $plugin_id;
12004 }
12005
12006 return $this->get_account_url( 'download_latest', $params );
12007 }
12008
12009 #endregion Download Plugin ------------------------------------------------------------------
12010
12011 /**
12012 * @author Vova Feldman (@svovaf)
12013 * @since 1.0.4
12014 *
12015 * @uses FS_Api
12016 *
12017 * @param bool $background Hints the method if it's a background updates check. If false, it means that
12018 * was initiated by the admin.
12019 * @param bool|number $plugin_id
12020 * @param bool $flush Since 1.1.7.3
12021 * @param int $expiration Since 1.2.2.7
12022 */
12023 private function check_updates(
12024 $background = false,
12025 $plugin_id = false,
12026 $flush = true,
12027 $expiration = WP_FS__TIME_24_HOURS_IN_SEC
12028 ) {
12029 $this->_logger->entrance();
12030
12031 // Check if there's a newer version for download.
12032 $new_version = $this->_fetch_newer_version( $plugin_id, $flush, $expiration );
12033
12034 $update = null;
12035 if ( is_object( $new_version ) ) {
12036 $update = new FS_Plugin_Tag( $new_version );
12037
12038 if ( ! $background ) {
12039 $this->_admin_notices->add(
12040 sprintf(
12041 /* translators: %s: Numeric version number (e.g. '2.1.9' */
12042 $this->get_text_inline( 'Version %s was released.', 'version-x-released' ) . ' ' . $this->get_text_inline( 'Please download %s.', 'please-download-x' ),
12043 $update->version,
12044 sprintf(
12045 '<a href="%s" target="_blank">%s</a>',
12046 $this->get_account_url( 'download_latest' ),
12047 sprintf(
12048 /* translators: %s: plan name (e.g. latest "Professional" version) */
12049 $this->get_text_inline( 'the latest %s version here', 'latest-x-version' ),
12050 $this->_site->plan->title
12051 )
12052 )
12053 ),
12054 $this->get_text_inline( 'New', 'new' ) . '!'
12055 );
12056 }
12057 } else if ( false === $new_version && ! $background ) {
12058 $this->_admin_notices->add(
12059 $this->get_text_inline( 'Seems like you got the latest release.', 'you-have-latest' ),
12060 $this->get_text_inline( 'You are all good!', 'you-are-good' )
12061 );
12062 }
12063
12064 $this->_store_update( $update, true, $plugin_id );
12065 }
12066
12067 /**
12068 * @author Vova Feldman (@svovaf)
12069 * @since 1.0.4
12070 *
12071 * @param bool $flush Since 1.1.7.3 add 24 hour cache by default.
12072 *
12073 * @return FS_Plugin[]
12074 *
12075 * @uses FS_Api
12076 */
12077 private function sync_addons( $flush = false ) {
12078 $this->_logger->entrance();
12079
12080 $api = $this->get_api_site_or_plugin_scope();
12081
12082 /**
12083 * @since 1.2.1
12084 *
12085 * If there's a cached version of the add-ons and not asking
12086 * for a flush, just use the currently stored add-ons.
12087 */
12088 if ( ! $flush && $api->is_cached( '/addons.json?enriched=true' ) ) {
12089 $addons = self::get_all_addons();
12090
12091 return $addons[ $this->_plugin->id ];
12092 }
12093
12094 $result = $api->get( '/addons.json?enriched=true', $flush );
12095
12096 $addons = array();
12097 if ( $this->is_api_result_object( $result, 'plugins' ) &&
12098 is_array( $result->plugins )
12099 ) {
12100 for ( $i = 0, $len = count( $result->plugins ); $i < $len; $i ++ ) {
12101 $addons[ $i ] = new FS_Plugin( $result->plugins[ $i ] );
12102 }
12103
12104 $this->_store_addons( $addons, true );
12105 }
12106
12107 return $addons;
12108 }
12109
12110 /**
12111 * Handle user email update.
12112 *
12113 * @author Vova Feldman (@svovaf)
12114 * @since 1.0.3
12115 * @uses FS_Api
12116 *
12117 * @param string $new_email
12118 *
12119 * @return object
12120 */
12121 private function update_email( $new_email ) {
12122 $this->_logger->entrance();
12123
12124
12125 $api = $this->get_api_user_scope();
12126 $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,email,is_verified", 'put', array(
12127 'email' => $new_email,
12128 'after_email_confirm_url' => $this->_get_admin_page_url(
12129 'account',
12130 array( 'fs_action' => 'sync_user' )
12131 ),
12132 ) );
12133
12134 if ( ! isset( $user->error ) ) {
12135 $this->_user->email = $user->email;
12136 $this->_user->is_verified = $user->is_verified;
12137 $this->_store_user();
12138 } else {
12139 // handle different error cases.
12140
12141 }
12142
12143 return $user;
12144 }
12145
12146 #----------------------------------------------------------------------------------
12147 #region API Error Handling
12148 #----------------------------------------------------------------------------------
12149
12150 /**
12151 * @author Vova Feldman (@svovaf)
12152 * @since 1.1.1
12153 *
12154 * @param mixed $result
12155 *
12156 * @return bool Is API result contains an error.
12157 */
12158 private function is_api_error( $result ) {
12159 return FS_Api::is_api_error( $result );
12160 }
12161
12162 /**
12163 * Checks if given API result is a non-empty and not an error object.
12164 *
12165 * @author Vova Feldman (@svovaf)
12166 * @since 1.2.1.5
12167 *
12168 * @param mixed $result
12169 * @param string|null $required_property Optional property we want to verify that is set.
12170 *
12171 * @return bool
12172 */
12173 function is_api_result_object( $result, $required_property = null ) {
12174 return FS_Api::is_api_result_object( $result, $required_property );
12175 }
12176
12177 /**
12178 * Checks if given API result is a non-empty entity object with non-empty ID.
12179 *
12180 * @author Vova Feldman (@svovaf)
12181 * @since 1.2.1.5
12182 *
12183 * @param mixed $result
12184 *
12185 * @return bool
12186 */
12187 private function is_api_result_entity( $result ) {
12188 return FS_Api::is_api_result_entity( $result );
12189 }
12190
12191 #endregion
12192
12193 /**
12194 * Make sure a given argument is an array of a specific type.
12195 *
12196 * @author Vova Feldman (@svovaf)
12197 * @since 1.2.1.5
12198 *
12199 * @param mixed $array
12200 * @param string $class
12201 *
12202 * @return bool
12203 */
12204 private function is_array_instanceof( $array, $class ) {
12205 return ( is_array( $array ) && ( empty( $array ) || $array[0] instanceof $class ) );
12206 }
12207
12208 /**
12209 * Start install ownership change.
12210 *
12211 * @author Vova Feldman (@svovaf)
12212 * @since 1.1.1
12213 * @uses FS_Api
12214 *
12215 * @param string $candidate_email
12216 *
12217 * @return bool Is ownership change successfully initiated.
12218 */
12219 private function init_change_owner( $candidate_email ) {
12220 $this->_logger->entrance();
12221
12222 $api = $this->get_api_site_scope();
12223 $result = $api->call( "/users/{$this->_user->id}.json", 'put', array(
12224 'email' => $candidate_email,
12225 'after_confirm_url' => $this->_get_admin_page_url(
12226 'account',
12227 array( 'fs_action' => 'change_owner' )
12228 ),
12229 ) );
12230
12231 return ! $this->is_api_error( $result );
12232 }
12233
12234 /**
12235 * Handle install ownership change.
12236 *
12237 * @author Vova Feldman (@svovaf)
12238 * @since 1.1.1
12239 * @uses FS_Api
12240 *
12241 * @return bool Was ownership change successfully complete.
12242 */
12243 private function complete_change_owner() {
12244 $this->_logger->entrance();
12245
12246 $site_result = $this->get_api_site_scope( true )->get();
12247 $site = new FS_Site( $site_result );
12248 $this->_site = $site;
12249
12250 $user = new FS_User();
12251 $user->id = fs_request_get( 'user_id' );
12252
12253 // Validate install's user and given user.
12254 if ( $user->id != $this->_site->user_id ) {
12255 return false;
12256 }
12257
12258 $user->public_key = fs_request_get( 'user_public_key' );
12259 $user->secret_key = fs_request_get( 'user_secret_key' );
12260
12261 // Fetch new user information.
12262 $this->_user = $user;
12263 $user_result = $this->get_api_user_scope( true )->get();
12264 $user = new FS_User( $user_result );
12265 $this->_user = $user;
12266
12267 $this->_set_account( $user, $site );
12268
12269 return true;
12270 }
12271
12272 /**
12273 * Handle user name update.
12274 *
12275 * @author Vova Feldman (@svovaf)
12276 * @since 1.0.9
12277 * @uses FS_Api
12278 *
12279 * @return object
12280 */
12281 private function update_user_name() {
12282 $this->_logger->entrance();
12283 $name = fs_request_get( 'fs_user_name_' . $this->get_unique_affix(), '' );
12284
12285 $api = $this->get_api_user_scope();
12286 $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,first,last", 'put', array(
12287 'name' => $name,
12288 ) );
12289
12290 if ( ! isset( $user->error ) ) {
12291 $this->_user->first = $user->first;
12292 $this->_user->last = $user->last;
12293 $this->_store_user();
12294 } else {
12295 // handle different error cases.
12296
12297 }
12298
12299 return $user;
12300 }
12301
12302 /**
12303 * Verify user email.
12304 *
12305 * @author Vova Feldman (@svovaf)
12306 * @since 1.0.3
12307 * @uses FS_Api
12308 */
12309 private function verify_email() {
12310 $this->_handle_account_user_sync();
12311
12312 if ( $this->_user->is_verified() ) {
12313 return;
12314 }
12315
12316 $api = $this->get_api_site_scope();
12317 $result = $api->call( "/users/{$this->_user->id}/verify.json", 'put', array(
12318 'after_email_confirm_url' => $this->_get_admin_page_url(
12319 'account',
12320 array( 'fs_action' => 'sync_user' )
12321 )
12322 ) );
12323
12324 if ( ! isset( $result->error ) ) {
12325 $this->_admin_notices->add( sprintf(
12326 $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' ),
12327 sprintf( '<a href="mailto:%1s">%2s</a>', esc_url( $this->_user->email ), $this->_user->email )
12328 ) );
12329 } else {
12330 // handle different error cases.
12331
12332 }
12333 }
12334
12335 /**
12336 * @author Vova Feldman (@svovaf)
12337 * @since 1.1.2
12338 *
12339 * @param array $params
12340 *
12341 * @return string
12342 */
12343 function get_activation_url( $params = array() ) {
12344 if ( $this->is_addon() && $this->has_free_plan() ) {
12345 /**
12346 * @author Vova Feldman (@svovaf)
12347 * @since 1.2.1.7 Add-on's activation is the parent's module activation.
12348 */
12349 return $this->get_parent_instance()->get_activation_url( $params );
12350 }
12351
12352 return $this->apply_filters( 'connect_url', $this->_get_admin_page_url( '', $params ) );
12353 }
12354
12355 /**
12356 * @author Vova Feldman (@svovaf)
12357 * @since 1.2.1.5
12358 *
12359 * @param array $params
12360 *
12361 * @return string
12362 */
12363 function get_reconnect_url( $params = array() ) {
12364 $params['fs_action'] = 'reset_anonymous_mode';
12365 $params['fs_unique_affix'] = $this->get_unique_affix();
12366
12367 return $this->get_activation_url( $params );
12368 }
12369
12370 /**
12371 * Get the URL of the page that should be loaded after the user connect
12372 * or skip in the opt-in screen.
12373 *
12374 * @author Vova Feldman (@svovaf)
12375 * @since 1.1.3
12376 *
12377 * @param string $filter Filter name.
12378 * @param array $params Since 1.2.2.7
12379 *
12380 * @return string
12381 */
12382 function get_after_activation_url( $filter, $params = array() ) {
12383 if ( $this->is_free_wp_org_theme() &&
12384 fs_request_has( 'pending_activation' )
12385 ) {
12386 $first_time_path = '';
12387 } else {
12388 $first_time_path = $this->_menu->get_first_time_path();
12389 }
12390
12391 return add_query_arg( $params, $this->apply_filters(
12392 $filter,
12393 empty( $first_time_path ) ?
12394 $this->_get_admin_page_url() :
12395 $first_time_path
12396 ) );
12397 }
12398
12399 /**
12400 * Handle account page updates / edits / actions.
12401 *
12402 * @author Vova Feldman (@svovaf)
12403 * @since 1.0.2
12404 *
12405 */
12406 private function _handle_account_edits() {
12407 if ( ! $this->is_user_admin() ) {
12408 return;
12409 }
12410
12411 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
12412 $action = fs_get_action();
12413
12414 // Alias.
12415 $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...';
12416
12417 switch ( $action ) {
12418 case 'delete_account':
12419 check_admin_referer( $action );
12420
12421 if ( $plugin_id == $this->get_id() ) {
12422 $this->delete_account_event();
12423
12424 // Clear user and site.
12425 $this->_site = null;
12426 $this->_user = null;
12427
12428 fs_redirect( $this->get_activation_url() );
12429 } else {
12430 if ( $this->is_addon_activated( $plugin_id ) ) {
12431 $fs_addon = self::get_instance_by_id( $plugin_id );
12432 $fs_addon->delete_account_event();
12433
12434 fs_redirect( $this->_get_admin_page_url( 'account' ) );
12435 }
12436 }
12437
12438 return;
12439
12440 case 'downgrade_account':
12441 check_admin_referer( $action );
12442
12443 if ( $plugin_id == $this->get_id() ) {
12444 $this->_downgrade_site();
12445 } else if ( $this->is_addon_activated( $plugin_id ) ) {
12446 $fs_addon = self::get_instance_by_id( $plugin_id );
12447 $fs_addon->_downgrade_site();
12448 }
12449
12450 return;
12451
12452 case 'activate_license':
12453 check_admin_referer( $action );
12454
12455 if ( $plugin_id == $this->get_id() ) {
12456 $this->_activate_license();
12457 } else {
12458 if ( $this->is_addon_activated( $plugin_id ) ) {
12459 $fs_addon = self::get_instance_by_id( $plugin_id );
12460 $fs_addon->_activate_license();
12461 }
12462 }
12463
12464 return;
12465
12466 case 'deactivate_license':
12467 check_admin_referer( $action );
12468
12469 if ( $plugin_id == $this->get_id() ) {
12470 $this->_deactivate_license();
12471
12472 if ( $this->is_only_premium() ) {
12473 // Clear user and site.
12474 $this->_site = null;
12475 $this->_user = null;
12476
12477 fs_redirect( $this->get_activation_url() );
12478 }
12479 } else {
12480 if ( $this->is_addon_activated( $plugin_id ) ) {
12481 $fs_addon = self::get_instance_by_id( $plugin_id );
12482 $fs_addon->_deactivate_license();
12483 }
12484 }
12485
12486 return;
12487
12488 case 'check_updates':
12489 check_admin_referer( $action );
12490 $this->check_updates();
12491
12492 return;
12493
12494 case 'change_owner':
12495 $state = fs_request_get( 'state', 'init' );
12496 switch ( $state ) {
12497 case 'init':
12498 $candidate_email = fs_request_get( 'candidate_email', '' );
12499
12500 if ( $this->init_change_owner( $candidate_email ) ) {
12501 $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>' ) );
12502 }
12503 break;
12504 case 'owner_confirmed':
12505 $candidate_email = fs_request_get( 'candidate_email', '' );
12506
12507 $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>' ) );
12508 break;
12509 case 'candidate_confirmed':
12510 if ( $this->complete_change_owner() ) {
12511 $this->_admin_notices->add_sticky(
12512 sprintf( $this->get_text_inline( '%s is the new owner of the account.', 'change-owner-request_candidate-confirmed' ), '<b>' . $this->_user->email . '</b>' ),
12513 'ownership_changed',
12514 $this->get_text_x_inline( 'Congrats', 'as congratulations', 'congrats' ) . '!'
12515 );
12516 } else {
12517 // @todo Handle failed ownership change message.
12518 }
12519 break;
12520 }
12521
12522 return;
12523
12524 case 'update_email':
12525 check_admin_referer( 'update_email' );
12526
12527 $new_email = fs_request_get( 'fs_email_' . $this->get_unique_affix(), '' );
12528 $result = $this->update_email( $new_email );
12529
12530 if ( isset( $result->error ) ) {
12531 switch ( $result->error->code ) {
12532 case 'user_exist':
12533 $this->_admin_notices->add(
12534 $this->get_text_inline( 'Sorry, we could not complete the email update. Another user with the same email is already registered.', 'user-exist-message' ) . ' ' .
12535 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>' ) .
12536 sprintf(
12537 '<a style="margin-left: 10px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
12538 $this->get_account_url( 'change_owner', array(
12539 'state' => 'init',
12540 'candidate_email' => $new_email
12541 ) ),
12542 $this->get_text_inline( 'Change Ownership', 'change-ownership' )
12543 ),
12544 $oops_text,
12545 'error'
12546 );
12547 break;
12548 }
12549 } else {
12550 $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' ) );
12551 }
12552
12553 return;
12554
12555 case 'update_user_name':
12556 check_admin_referer( 'update_user_name' );
12557
12558 $result = $this->update_user_name();
12559
12560 if ( isset( $result->error ) ) {
12561 $this->_admin_notices->add(
12562 $this->get_text_inline( 'Please provide your full name.', 'name-update-failed-message' ),
12563 $oops_text,
12564 'error'
12565 );
12566 } else {
12567 $this->_admin_notices->add( $this->get_text_inline( 'Your name was successfully updated.', 'name-updated-message' ) );
12568 }
12569
12570 return;
12571
12572 #region Actions that might be called from external links (e.g. email)
12573
12574 case 'cancel_trial':
12575 if ( $plugin_id == $this->get_id() ) {
12576 $this->_cancel_trial();
12577 } else {
12578 if ( $this->is_addon_activated( $plugin_id ) ) {
12579 $fs_addon = self::get_instance_by_id( $plugin_id );
12580 $fs_addon->_cancel_trial();
12581 }
12582 }
12583
12584 return;
12585
12586 case 'verify_email':
12587 $this->verify_email();
12588
12589 return;
12590
12591 case 'sync_user':
12592 $this->_handle_account_user_sync();
12593
12594 return;
12595
12596 case $this->get_unique_affix() . '_sync_license':
12597 $this->_sync_license();
12598
12599 return;
12600
12601 case 'download_latest':
12602 $this->download_latest_directly( $plugin_id );
12603
12604 return;
12605
12606 #endregion
12607 }
12608
12609 if ( WP_FS__IS_POST_REQUEST ) {
12610 $properties = array( 'site_secret_key', 'site_id', 'site_public_key' );
12611 foreach ( $properties as $p ) {
12612 if ( 'update_' . $p === $action ) {
12613 check_admin_referer( $action );
12614
12615 $this->_logger->log( $action );
12616
12617 $site_property = substr( $p, strlen( 'site_' ) );
12618 $site_property_value = fs_request_get( 'fs_' . $p . '_' . $this->get_unique_affix(), '' );
12619 $this->get_site()->{$site_property} = $site_property_value;
12620
12621 // Store account after modification.
12622 $this->_store_site();
12623
12624 $this->do_action( 'account_property_edit', 'site', $site_property, $site_property_value );
12625
12626 $this->_admin_notices->add( sprintf(
12627 /* translators: %s: User's account property (e.g. email address, name) */
12628 $this->get_text_inline( 'You have successfully updated your %s.', 'x-updated' ),
12629 '<b>' . str_replace( '_', ' ', $p ) . '</b>'
12630 ) );
12631
12632 return;
12633 }
12634 }
12635 }
12636 }
12637
12638 /**
12639 * Account page resources load.
12640 *
12641 * @author Vova Feldman (@svovaf)
12642 * @since 1.0.6
12643 */
12644 function _account_page_load() {
12645 $this->_logger->entrance();
12646
12647 $this->_logger->info( var_export( $_REQUEST, true ) );
12648
12649 fs_enqueue_local_style( 'fs_account', '/admin/account.css' );
12650
12651 if ( $this->has_addons() ) {
12652 wp_enqueue_script( 'plugin-install' );
12653 add_thickbox();
12654
12655 function fs_addons_body_class( $classes ) {
12656 $classes .= ' plugins-php';
12657
12658 return $classes;
12659 }
12660
12661 add_filter( 'admin_body_class', 'fs_addons_body_class' );
12662 }
12663
12664 if ( $this->has_paid_plan() &&
12665 ! $this->has_any_license() &&
12666 ! $this->is_sync_executed() &&
12667 $this->is_tracking_allowed()
12668 ) {
12669 /**
12670 * If no licenses found and no sync job was executed during the last 24 hours,
12671 * just execute the sync job right away (blocking execution).
12672 *
12673 * @since 1.1.7.3
12674 */
12675 $this->run_manual_sync();
12676 }
12677
12678 $this->_handle_account_edits();
12679
12680 $this->do_action( 'account_page_load_before_departure' );
12681 }
12682
12683 /**
12684 * Renders the "Affiliation" page.
12685 *
12686 * @author Leo Fajardo (@leorw)
12687 * @since 1.2.3
12688 */
12689 function _affiliation_page_render() {
12690 $this->_logger->entrance();
12691
12692 $this->fetch_affiliate_and_terms();
12693
12694 fs_enqueue_local_style( 'fs_affiliation', '/admin/affiliation.css' );
12695
12696 $vars = array( 'id' => $this->_module_id );
12697 echo $this->apply_filters( "/forms/affiliation.php", fs_get_template( '/forms/affiliation.php', $vars ) );
12698 }
12699
12700
12701 /**
12702 * Render account page.
12703 *
12704 * @author Vova Feldman (@svovaf)
12705 * @since 1.0.0
12706 */
12707 function _account_page_render() {
12708 $this->_logger->entrance();
12709
12710 $template = 'account.php';
12711 $vars = array( 'id' => $this->_module_id );
12712
12713 /**
12714 * Added filter to the template to allow developers wrapping the template
12715 * in custom HTML (e.g. within a wizard/tabs).
12716 *
12717 * @author Vova Feldman (@svovaf)
12718 * @since 1.2.1.6
12719 */
12720 echo $this->apply_filters( "templates/{$template}", fs_get_template( $template, $vars ) );
12721 }
12722
12723 /**
12724 * Render account connect page.
12725 *
12726 * @author Vova Feldman (@svovaf)
12727 * @since 1.0.7
12728 */
12729 function _connect_page_render() {
12730 $this->_logger->entrance();
12731
12732 $vars = array( 'id' => $this->_module_id );
12733
12734 /**
12735 * Added filter to the template to allow developers wrapping the template
12736 * in custom HTML (e.g. within a wizard/tabs).
12737 *
12738 * @author Vova Feldman (@svovaf)
12739 * @since 1.2.1.6
12740 */
12741 echo $this->apply_filters( 'templates/connect.php', fs_get_template( 'connect.php', $vars ) );
12742 }
12743
12744 /**
12745 * Load required resources before add-ons page render.
12746 *
12747 * @author Vova Feldman (@svovaf)
12748 * @since 1.0.6
12749 */
12750 function _addons_page_load() {
12751 $this->_logger->entrance();
12752
12753 fs_enqueue_local_style( 'fs_addons', '/admin/add-ons.css' );
12754
12755 wp_enqueue_script( 'plugin-install' );
12756 add_thickbox();
12757
12758 function fs_addons_body_class( $classes ) {
12759 $classes .= ' plugins-php';
12760
12761 return $classes;
12762 }
12763
12764 add_filter( 'admin_body_class', 'fs_addons_body_class' );
12765
12766 if ( ! $this->is_registered() && $this->is_org_repo_compliant() ) {
12767 $this->_admin_notices->add(
12768 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>' ),
12769 $this->get_text_x_inline( 'Heads up', 'advance notice of something that will need attention.', 'heads-up' ),
12770 'update-nag'
12771 );
12772 }
12773 }
12774
12775 /**
12776 * Render add-ons page.
12777 *
12778 * @author Vova Feldman (@svovaf)
12779 * @since 1.0.6
12780 */
12781 function _addons_page_render() {
12782 $this->_logger->entrance();
12783
12784 $vars = array( 'id' => $this->_module_id );
12785
12786 /**
12787 * Added filter to the template to allow developers wrapping the template
12788 * in custom HTML (e.g. within a wizard/tabs).
12789 *
12790 * @author Vova Feldman (@svovaf)
12791 * @since 1.2.1.6
12792 */
12793 echo $this->apply_filters( 'templates/add-ons.php', fs_get_template( 'add-ons.php', $vars ) );
12794 }
12795
12796 /* Pricing & Upgrade
12797 ------------------------------------------------------------------------------------------------------------------*/
12798 /**
12799 * Render pricing page.
12800 *
12801 * @author Vova Feldman (@svovaf)
12802 * @since 1.0.0
12803 */
12804 function _pricing_page_render() {
12805 $this->_logger->entrance();
12806
12807 $vars = array( 'id' => $this->_module_id );
12808
12809 if ( 'true' === fs_request_get( 'checkout', false ) ) {
12810 fs_require_once_template( 'checkout.php', $vars );
12811 } else {
12812 fs_require_once_template( 'pricing.php', $vars );
12813 }
12814 }
12815
12816 #----------------------------------------------------------------------------------
12817 #region Contact Us
12818 #----------------------------------------------------------------------------------
12819
12820 /**
12821 * Render contact-us page.
12822 *
12823 * @author Vova Feldman (@svovaf)
12824 * @since 1.0.3
12825 */
12826 function _contact_page_render() {
12827 $this->_logger->entrance();
12828
12829 $vars = array( 'id' => $this->_module_id );
12830 fs_require_once_template( 'contact.php', $vars );
12831 }
12832
12833 #endregion ------------------------------------------------------------------------
12834
12835 /**
12836 * Hide all admin notices to prevent distractions.
12837 *
12838 * @author Vova Feldman (@svovaf)
12839 * @since 1.0.3
12840 *
12841 * @uses remove_all_actions()
12842 */
12843 private static function _hide_admin_notices() {
12844 remove_all_actions( 'admin_notices' );
12845 remove_all_actions( 'network_admin_notices' );
12846 remove_all_actions( 'all_admin_notices' );
12847 remove_all_actions( 'user_admin_notices' );
12848 }
12849
12850 static function _clean_admin_content_section_hook() {
12851 self::_hide_admin_notices();
12852
12853 // Hide footer.
12854 echo '<style>#wpfooter { display: none !important; }</style>';
12855 }
12856
12857 /**
12858 * Attach to admin_head hook to hide all admin notices.
12859 *
12860 * @author Vova Feldman (@svovaf)
12861 * @since 1.0.3
12862 */
12863 static function _clean_admin_content_section() {
12864 add_action( 'admin_head', 'Freemius::_clean_admin_content_section_hook' );
12865 }
12866
12867 /* CSS & JavaScript
12868 ------------------------------------------------------------------------------------------------------------------*/
12869 /* function _enqueue_script($handle, $src) {
12870 $url = plugins_url( substr( WP_FS__DIR_JS, strlen( $this->_plugin_dir_path ) ) . '/assets/js/' . $src );
12871
12872 $this->_logger->entrance( 'script = ' . $url );
12873
12874 wp_enqueue_script( $handle, $url );
12875 }*/
12876
12877 /* SDK
12878 ------------------------------------------------------------------------------------------------------------------*/
12879 private $_user_api;
12880
12881 /**
12882 *
12883 * @author Vova Feldman (@svovaf)
12884 * @since 1.0.2
12885 *
12886 * @param bool $flush
12887 *
12888 * @return FS_Api
12889 */
12890 function get_api_user_scope( $flush = false ) {
12891 if ( ! isset( $this->_user_api ) || $flush ) {
12892 $this->_user_api = FS_Api::instance(
12893 $this->_module_id,
12894 'user',
12895 $this->_user->id,
12896 $this->_user->public_key,
12897 ! $this->is_live(),
12898 $this->_user->secret_key
12899 );
12900 }
12901
12902 return $this->_user_api;
12903 }
12904
12905 private $_site_api;
12906
12907 /**
12908 *
12909 * @author Vova Feldman (@svovaf)
12910 * @since 1.0.2
12911 *
12912 * @param bool $flush
12913 *
12914 * @return FS_Api
12915 */
12916 function get_api_site_scope( $flush = false ) {
12917 if ( ! isset( $this->_site_api ) || $flush ) {
12918 $this->_site_api = FS_Api::instance(
12919 $this->_module_id,
12920 'install',
12921 $this->_site->id,
12922 $this->_site->public_key,
12923 ! $this->is_live(),
12924 $this->_site->secret_key
12925 );
12926 }
12927
12928 return $this->_site_api;
12929 }
12930
12931 private $_plugin_api;
12932
12933 /**
12934 * Get plugin public API scope.
12935 *
12936 * @author Vova Feldman (@svovaf)
12937 * @since 1.0.7
12938 *
12939 * @return FS_Api
12940 */
12941 function get_api_plugin_scope() {
12942 if ( ! isset( $this->_plugin_api ) ) {
12943 $this->_plugin_api = FS_Api::instance(
12944 $this->_module_id,
12945 'plugin',
12946 $this->_plugin->id,
12947 $this->_plugin->public_key,
12948 ! $this->is_live()
12949 );
12950 }
12951
12952 return $this->_plugin_api;
12953 }
12954
12955 /**
12956 * Get site API scope object (fallback to public plugin scope when not registered).
12957 *
12958 * @author Vova Feldman (@svovaf)
12959 * @since 1.0.7
12960 *
12961 * @return FS_Api
12962 */
12963 function get_api_site_or_plugin_scope() {
12964 return $this->is_registered() ?
12965 $this->get_api_site_scope() :
12966 $this->get_api_plugin_scope();
12967 }
12968
12969 /**
12970 * Show trial promotional notice (if any trial exist).
12971 *
12972 * @author Vova Feldman (@svovaf)
12973 * @since 1.0.9
12974 *
12975 * @param $plans
12976 */
12977 function _check_for_trial_plans( $plans ) {
12978 $this->_storage->has_trial_plan = FS_Plan_Manager::instance()->has_trial_plan( $plans );
12979 }
12980
12981 /**
12982 * During trial promotion the "upgrade" submenu item turns to
12983 * "start trial" to encourage the trial. Since we want to keep
12984 * the same menu item handler and there's no robust way to
12985 * add new arguments to the menu item link's querystring,
12986 * use JavaScript to find the menu item and update the href of
12987 * the link.
12988 *
12989 * @author Vova Feldman (@svovaf)
12990 * @since 1.2.1.5
12991 */
12992 function _fix_start_trial_menu_item_url() {
12993 $template_args = array( 'id' => $this->_module_id );
12994 fs_require_template( 'add-trial-to-pricing.php', $template_args );
12995 }
12996
12997 /**
12998 * Check if module is currently in a trial promotion mode.
12999 *
13000 * @author Vova Feldman (@svovaf)
13001 * @since 1.2.2.7
13002 *
13003 * @return bool
13004 */
13005 function is_in_trial_promotion() {
13006 return $this->_admin_notices->has_sticky( 'trial_promotion' );
13007 }
13008
13009 /**
13010 * Show trial promotional notice (if any trial exist).
13011 *
13012 * @author Vova Feldman (@svovaf)
13013 * @since 1.0.9
13014 *
13015 * @return bool If trial notice added.
13016 */
13017 function _add_trial_notice() {
13018 if ( ! $this->is_user_admin() ) {
13019 return false;
13020 }
13021
13022 if ( ! $this->is_user_in_admin() ) {
13023 return false;
13024 }
13025
13026 // Check if trial message is already shown.
13027 if ( $this->is_in_trial_promotion() ) {
13028 add_action( 'admin_footer', array( &$this, '_fix_start_trial_menu_item_url' ) );
13029
13030 $this->_menu->add_counter_to_menu_item( 1, 'fs-trial' );
13031
13032 return false;
13033 }
13034
13035 if ( $this->is_premium() && ! WP_FS__DEV_MODE ) {
13036 // Don't show trial if running the premium code, unless running in DEV mode.
13037 return false;
13038 }
13039
13040 if ( ! $this->has_trial_plan() ) {
13041 // No plans with trial.
13042 return false;
13043 }
13044
13045 if ( ! $this->apply_filters( 'show_trial', true ) ) {
13046 // Developer explicitly asked not to show the trial promo.
13047 return false;
13048 }
13049
13050 if ( $this->is_registered() ) {
13051 // Check if trial already utilized.
13052 if ( $this->_site->is_trial_utilized() ) {
13053 return false;
13054 }
13055
13056 if ( $this->is_paying_or_trial() ) {
13057 // Don't show trial if paying or already in trial.
13058 return false;
13059 }
13060 }
13061
13062 if ( $this->is_activation_mode() || $this->is_pending_activation() ) {
13063 // If not yet opted-in/skipped, or pending activation, don't show trial.
13064 return false;
13065 }
13066
13067 $last_time_trial_promotion_shown = $this->_storage->get( 'trial_promotion_shown', false );
13068 $was_promotion_shown_before = ( false !== $last_time_trial_promotion_shown );
13069
13070 // Show promotion if never shown before and 24 hours after initial activation with FS.
13071 if ( ! $was_promotion_shown_before &&
13072 $this->_storage->install_timestamp > ( time() - WP_FS__TIME_24_HOURS_IN_SEC )
13073 ) {
13074 return false;
13075 }
13076
13077 // OR if promotion was shown before, try showing it every 30 days.
13078 if ( $was_promotion_shown_before &&
13079 30 * WP_FS__TIME_24_HOURS_IN_SEC > time() - $last_time_trial_promotion_shown
13080 ) {
13081 return false;
13082 }
13083
13084 $trial_period = $this->_trial_days;
13085 $require_payment = $this->_is_trial_require_payment;
13086 $trial_url = $this->get_trial_url();
13087 $plans_string = strtolower( $this->get_text_inline( 'Awesome', 'awesome' ) );
13088
13089 if ( $this->is_registered() ) {
13090 // If opted-in, override trial with up to date data from API.
13091 $trial_plans = FS_Plan_Manager::instance()->get_trial_plans( $this->_plans );
13092 $trial_plans_count = count( $trial_plans );
13093
13094 if ( 0 === $trial_plans_count ) {
13095 // If there's no plans with a trial just exit.
13096 return false;
13097 }
13098
13099 /**
13100 * @var FS_Plugin_Plan $paid_plan
13101 */
13102 $paid_plan = $trial_plans[0];
13103 $require_payment = $paid_plan->is_require_subscription;
13104 $trial_period = $paid_plan->trial_period;
13105
13106 $total_paid_plans = count( $this->_plans ) - ( FS_Plan_Manager::instance()->has_free_plan( $this->_plans ) ? 1 : 0 );
13107
13108 if ( $total_paid_plans !== $trial_plans_count ) {
13109 // Not all paid plans have a trial - generate a string of those that have it.
13110 for ( $i = 0; $i < $trial_plans_count; $i ++ ) {
13111 $plans_string .= sprintf(
13112 ' <a href="%s">%s</a>',
13113 $trial_url,
13114 $trial_plans[ $i ]->title
13115 );
13116
13117 if ( $i < $trial_plans_count - 2 ) {
13118 $plans_string .= ', ';
13119 } else if ( $i == $trial_plans_count - 2 ) {
13120 $plans_string .= ' and ';
13121 }
13122 }
13123 }
13124 }
13125
13126 $message = sprintf(
13127 $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' ),
13128 sprintf( '<b>%s</b>', $this->get_plugin_name() ),
13129 $plans_string,
13130 $trial_period
13131 );
13132
13133 // "No Credit-Card Required" or "No Commitment for N Days".
13134 $cc_string = $require_payment ?
13135 sprintf( $this->get_text_inline( 'No commitment for %s days - cancel anytime!', 'no-commitment-for-x-days' ), $trial_period ) :
13136 $this->get_text_inline( 'No credit card required', 'no-cc-required' ) . '!';
13137
13138
13139 // Start trial button.
13140 $button = ' ' . sprintf(
13141 '<a style="margin-left: 10px; vertical-align: super;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
13142 $trial_url,
13143 $this->get_text_x_inline( 'Start free trial', 'call to action', 'start-free-trial' )
13144 );
13145
13146 $this->_admin_notices->add_sticky(
13147 $this->apply_filters( 'trial_promotion_message', "{$message} {$cc_string} {$button}" ),
13148 'trial_promotion',
13149 '',
13150 'promotion'
13151 );
13152
13153 $this->_storage->trial_promotion_shown = WP_FS__SCRIPT_START_TIME;
13154
13155 return true;
13156 }
13157
13158 /**
13159 * Lets users/customers know that the product has an affiliate program.
13160 *
13161 * @author Leo Fajardo (@leorw)
13162 * @since 1.2.2.11
13163 *
13164 * @return bool Returns true if the notice has been added.
13165 */
13166 function _add_affiliate_program_notice() {
13167 if ( ! $this->is_user_admin() ) {
13168 return false;
13169 }
13170
13171 if ( ! $this->is_user_in_admin() ) {
13172 return false;
13173 }
13174
13175 // Check if the notice is already shown.
13176 if ( $this->_admin_notices->has_sticky( 'affiliate_program' ) ) {
13177 return false;
13178 }
13179
13180 if (
13181 // Product has no affiliate program.
13182 ! $this->has_affiliate_program() ||
13183 // User has applied for an affiliate account.
13184 ! empty( $this->_storage->affiliate_application_data ) ) {
13185 return false;
13186 }
13187
13188 if ( ! $this->apply_filters( 'show_affiliate_program_notice', true ) ) {
13189 // Developer explicitly asked not to show the notice about the affiliate program.
13190 return false;
13191 }
13192
13193 if ( $this->is_activation_mode() || $this->is_pending_activation() ) {
13194 // If not yet opted in/skipped, or pending activation, don't show the notice.
13195 return false;
13196 }
13197
13198 $last_time_notice_was_shown = $this->_storage->get( 'affiliate_program_notice_shown', false );
13199 $was_notice_shown_before = ( false !== $last_time_notice_was_shown );
13200
13201 /**
13202 * Do not show the notice if it was already shown before or less than 30 days have passed since the initial
13203 * activation with FS.
13204 */
13205 if ( $was_notice_shown_before ||
13206 $this->_storage->install_timestamp > ( time() - ( WP_FS__TIME_24_HOURS_IN_SEC * 30 ) )
13207 ) {
13208 return false;
13209 }
13210
13211 if ( ! $this->is_paying() &&
13212 FS_Plugin::AFFILIATE_MODERATION_CUSTOMERS == $this->_plugin->affiliate_moderation ) {
13213 // If the user is not a customer and the affiliate program is only for customers, don't show the notice.
13214 return false;
13215 }
13216
13217 $message = sprintf(
13218 $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' ),
13219 sprintf( '<strong>%s</strong>', $this->get_plugin_name() ),
13220 $this->get_module_label( true )
13221 );
13222
13223 // HTML code for the "Learn more..." button.
13224 $button = ' ' . sprintf(
13225 '<a style="display: block; margin-top: 10px;" href="%s"><button class="button button-primary">%s &nbsp;&#10140;</button></a>',
13226 $this->_get_admin_page_url( 'affiliation' ),
13227 $this->get_text_inline( 'Learn more', 'learn-more' ) . '...'
13228 );
13229
13230 $this->_admin_notices->add_sticky(
13231 $this->apply_filters( 'affiliate_program_notice', "{$message} {$button}" ),
13232 'affiliate_program',
13233 '',
13234 'promotion'
13235 );
13236
13237 $this->_storage->affiliate_program_notice_shown = WP_FS__SCRIPT_START_TIME;
13238
13239 return true;
13240 }
13241
13242 /**
13243 * @author Vova Feldman (@svovaf)
13244 * @since 1.2.1.5
13245 */
13246 function _enqueue_common_css() {
13247 if ( $this->has_paid_plan() && ! $this->is_paying() ) {
13248 // Add basic CSS for admin-notices and menu-item colors.
13249 fs_enqueue_local_style( 'fs_common', '/admin/common.css' );
13250 }
13251 }
13252
13253 /**
13254 * @author Leo Fajardo (leorw)
13255 * @since 1.2.2
13256 */
13257 function _show_theme_activation_optin_dialog() {
13258 fs_enqueue_local_style( 'fs_connect', '/admin/connect.css' );
13259
13260 add_action( 'admin_footer-themes.php', array( &$this, '_add_fs_theme_activation_dialog' ) );
13261 }
13262
13263 /**
13264 * @author Leo Fajardo (leorw)
13265 * @since 1.2.2
13266 */
13267 function _add_fs_theme_activation_dialog() {
13268 $vars = array( 'id' => $this->_module_id );
13269 fs_require_once_template( 'connect.php', $vars );
13270 }
13271
13272 /* Action Links
13273 ------------------------------------------------------------------------------------------------------------------*/
13274 private $_action_links_hooked = false;
13275 private $_action_links = array();
13276
13277 /**
13278 * Hook to plugin action links filter.
13279 *
13280 * @author Vova Feldman (@svovaf)
13281 * @since 1.0.0
13282 */
13283 private function hook_plugin_action_links() {
13284 $this->_logger->entrance();
13285
13286 $this->_action_links_hooked = true;
13287
13288 $this->_logger->log( 'Adding action links hooks.' );
13289
13290 // Add action link to settings page.
13291 add_filter( 'plugin_action_links_' . $this->_plugin_basename, array(
13292 &$this,
13293 '_modify_plugin_action_links_hook'
13294 ), WP_FS__DEFAULT_PRIORITY, 2 );
13295 add_filter( 'network_admin_plugin_action_links_' . $this->_plugin_basename, array(
13296 &$this,
13297 '_modify_plugin_action_links_hook'
13298 ), WP_FS__DEFAULT_PRIORITY, 2 );
13299 }
13300
13301 /**
13302 * Add plugin action link.
13303 *
13304 * @author Vova Feldman (@svovaf)
13305 * @since 1.0.0
13306 *
13307 * @param $label
13308 * @param $url
13309 * @param bool $external
13310 * @param int $priority
13311 * @param bool $key
13312 */
13313 function add_plugin_action_link( $label, $url, $external = false, $priority = WP_FS__DEFAULT_PRIORITY, $key = false ) {
13314 $this->_logger->entrance();
13315
13316 if ( ! isset( $this->_action_links[ $priority ] ) ) {
13317 $this->_action_links[ $priority ] = array();
13318 }
13319
13320 if ( false === $key ) {
13321 $key = preg_replace( "/[^A-Za-z0-9 ]/", '', strtolower( $label ) );
13322 }
13323
13324 $this->_action_links[ $priority ][] = array(
13325 'label' => $label,
13326 'href' => $url,
13327 'key' => $key,
13328 'external' => $external
13329 );
13330 }
13331
13332 /**
13333 * Adds Upgrade and Add-Ons links to the main Plugins page link actions collection.
13334 *
13335 * @author Vova Feldman (@svovaf)
13336 * @since 1.0.0
13337 */
13338 function _add_upgrade_action_link() {
13339 $this->_logger->entrance();
13340
13341 if ( $this->is_registered() ) {
13342 if ( ! $this->is_paying() && $this->has_paid_plan() ) {
13343 $this->add_plugin_action_link(
13344 $this->get_text_inline( 'Upgrade', 'upgrade' ),
13345 $this->get_upgrade_url(),
13346 false,
13347 7,
13348 'upgrade'
13349 );
13350 }
13351
13352 if ( $this->has_addons() ) {
13353 $this->add_plugin_action_link(
13354 $this->get_text_inline( 'Add-Ons', 'add-ons' ),
13355 $this->_get_admin_page_url( 'addons' ),
13356 false,
13357 9,
13358 'addons'
13359 );
13360 }
13361 }
13362 }
13363
13364 /**
13365 * Adds "Activate License" or "Change License" link to the main Plugins page link actions collection.
13366 *
13367 * @author Leo Fajardo (@leorw)
13368 * @since 1.1.9
13369 */
13370 function _add_license_action_link() {
13371 $this->_logger->entrance();
13372
13373 if ( $this->is_free_plan() && $this->is_addon() ) {
13374 return;
13375 }
13376
13377 if ( ! self::is_ajax() ) {
13378 // Inject license activation dialog UI and client side code.
13379 add_action( 'admin_footer', array( &$this, '_add_license_activation_dialog_box' ) );
13380 }
13381
13382 $link_text = $this->is_free_plan() ?
13383 $this->get_text_inline( 'Activate License', 'activate-license' ) :
13384 $this->get_text_inline( 'Change License', 'change-license' );
13385
13386 $this->add_plugin_action_link(
13387 $link_text,
13388 '#',
13389 false,
13390 11,
13391 ( 'activate-license ' . $this->get_unique_affix() )
13392 );
13393 }
13394
13395 /**
13396 * Adds "Opt in" or "Opt out" link to the main "Plugins" page link actions collection.
13397 *
13398 * @author Leo Fajardo (@leorw)
13399 * @since 1.2.1.5
13400 */
13401 function _add_tracking_links() {
13402 if ( ! current_user_can( 'activate_plugins' ) ) {
13403 return;
13404 }
13405
13406 $this->_logger->entrance();
13407
13408 if ( fs_request_is_action_secure( $this->get_unique_affix() . '_reconnect' ) ) {
13409 if ( ! $this->is_registered() && $this->is_anonymous() ) {
13410 $this->connect_again();
13411
13412 return;
13413 }
13414 }
13415
13416 if ( ( $this->is_plugin() && ! self::is_plugins_page() ) ||
13417 ( $this->is_theme() && ! self::is_themes_page() )
13418 ) {
13419 // Only show tracking links on the plugins and themes pages.
13420 return;
13421 }
13422
13423 if ( ! $this->is_enable_anonymous() ) {
13424 // Don't allow to opt-out if anonymous mode is disabled.
13425 return;
13426 }
13427
13428 if ( ! $this->is_free_plan() ) {
13429 // Don't allow to opt-out if running in paid plan.
13430 return;
13431 }
13432
13433 if ( $this->add_ajax_action( 'stop_tracking', array( &$this, '_stop_tracking_callback' ) ) ) {
13434 return;
13435 }
13436
13437 if ( $this->add_ajax_action( 'allow_tracking', array( &$this, '_allow_tracking_callback' ) ) ) {
13438 return;
13439 }
13440
13441 $url = '#';
13442
13443 if ( $this->is_registered() ) {
13444 if ( $this->is_tracking_allowed() ) {
13445 $link_text_id = $this->get_text_inline( 'Opt Out', 'opt-out' );
13446 } else {
13447 $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' );
13448 }
13449
13450 add_action( 'admin_footer', array( &$this, '_add_optout_dialog' ) );
13451 } else {
13452 $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' );
13453
13454 $params = ! $this->is_anonymous() ?
13455 array() :
13456 array(
13457 'nonce' => wp_create_nonce( $this->get_unique_affix() . '_reconnect' ),
13458 'fs_action' => ( $this->get_unique_affix() . '_reconnect' ),
13459 );
13460
13461 $url = $this->get_activation_url( $params );
13462 }
13463
13464 if ( $this->is_plugin() && self::is_plugins_page() ) {
13465 $this->add_plugin_action_link(
13466 $link_text_id,
13467 $url,
13468 false,
13469 13,
13470 "opt-in-or-opt-out {$this->_slug}"
13471 );
13472 }
13473 }
13474
13475 /**
13476 * Get the URL of the page that should be loaded right after the plugin activation.
13477 *
13478 * @author Vova Feldman (@svovaf)
13479 * @since 1.1.7.4
13480 *
13481 * @return string
13482 */
13483 function get_after_plugin_activation_redirect_url() {
13484 $url = false;
13485
13486 if ( ! $this->is_addon() || ! $this->has_free_plan() ) {
13487 $first_time_path = $this->_menu->get_first_time_path();
13488 $url = $this->is_activation_mode() ?
13489 $this->get_activation_url() :
13490 ( empty( $first_time_path ) ?
13491 $this->_get_admin_page_url() :
13492 $first_time_path );
13493 } else {
13494 $plugin_fs = false;
13495
13496 if ( $this->is_parent_plugin_installed() ) {
13497 $plugin_fs = self::get_parent_instance();
13498 }
13499
13500 if ( is_object( $plugin_fs ) ) {
13501 if ( ! $plugin_fs->is_registered() ) {
13502 // Forward to parent plugin connect when parent not registered.
13503 $url = $plugin_fs->get_activation_url();
13504 } else {
13505 // Forward to account page.
13506 $url = $plugin_fs->_get_admin_page_url( 'account' );
13507 }
13508 }
13509 }
13510
13511 return $url;
13512 }
13513
13514 /**
13515 * Forward page to activation page.
13516 *
13517 * @author Vova Feldman (@svovaf)
13518 * @since 1.0.3
13519 */
13520 function _redirect_on_activation_hook() {
13521 $url = $this->get_after_plugin_activation_redirect_url();
13522
13523 if ( is_string( $url ) ) {
13524 fs_redirect( $url );
13525 }
13526 }
13527
13528 /**
13529 * Modify plugin's page action links collection.
13530 *
13531 * @author Vova Feldman (@svovaf)
13532 * @since 1.0.0
13533 *
13534 * @param array $links
13535 * @param $file
13536 *
13537 * @return array
13538 */
13539 function _modify_plugin_action_links_hook( $links, $file ) {
13540 $this->_logger->entrance();
13541
13542 $passed_deactivate = false;
13543 $deactivate_link = '';
13544 $before_deactivate = array();
13545 $after_deactivate = array();
13546 foreach ( $links as $key => $link ) {
13547 if ( 'deactivate' === $key ) {
13548 $deactivate_link = $link;
13549 $passed_deactivate = true;
13550 continue;
13551 }
13552
13553 if ( ! $passed_deactivate ) {
13554 $before_deactivate[ $key ] = $link;
13555 } else {
13556 $after_deactivate[ $key ] = $link;
13557 }
13558 }
13559
13560 ksort( $this->_action_links );
13561
13562 foreach ( $this->_action_links as $new_links ) {
13563 foreach ( $new_links as $link ) {
13564 $before_deactivate[ $link['key'] ] = '<a href="' . $link['href'] . '"' . ( $link['external'] ? ' target="_blank"' : '' ) . '>' . $link['label'] . '</a>';
13565 }
13566 }
13567
13568 if ( ! empty( $deactivate_link ) ) {
13569 /**
13570 * This HTML element is used to identify the correct plugin when attaching an event to its Deactivate link.
13571 *
13572 * @since 1.2.1.6 Always show the deactivation feedback form since we added automatic free version deactivation upon premium code activation.
13573 */
13574 $deactivate_link .= '<i class="fs-module-id" data-module-id="' . $this->_module_id . '"></i>';
13575
13576 // Append deactivation link.
13577 $before_deactivate['deactivate'] = $deactivate_link;
13578 }
13579
13580 return array_merge( $before_deactivate, $after_deactivate );
13581 }
13582
13583 /**
13584 * Adds admin message.
13585 *
13586 * @author Vova Feldman (@svovaf)
13587 * @since 1.0.4
13588 *
13589 * @param string $message
13590 * @param string $title
13591 * @param string $type
13592 */
13593 function add_admin_message( $message, $title = '', $type = 'success' ) {
13594 $this->_admin_notices->add( $message, $title, $type );
13595 }
13596
13597 /**
13598 * Adds sticky admin message.
13599 *
13600 * @author Vova Feldman (@svovaf)
13601 * @since 1.1.0
13602 *
13603 * @param string $message
13604 * @param string $id
13605 * @param string $title
13606 * @param string $type
13607 */
13608 function add_sticky_admin_message( $message, $id, $title = '', $type = 'success' ) {
13609 $this->_admin_notices->add_sticky( $message, $id, $title, $type );
13610 }
13611
13612 /**
13613 * Helper function that returns the final steps for the upgrade completion.
13614 *
13615 * If the module is already running the premium code, returns an empty string.
13616 *
13617 * @author Vova Feldman (@svovaf)
13618 * @since 1.2.1
13619 *
13620 * @param string $plan_title
13621 *
13622 * @return string
13623 */
13624 private function get_complete_upgrade_instructions( $plan_title = '' ) {
13625 if ( ! $this->has_premium_version() || $this->is_premium() ) {
13626 return '';
13627 }
13628
13629 if ( empty( $plan_title ) ) {
13630 $plan_title = $this->_site->plan->title;
13631 }
13632
13633 // @since 1.2.1.5 The free version is auto deactivated.
13634 $deactivation_step = version_compare( $this->version, '1.2.1.5', '<' ) ?
13635 ( '<li>' . $this->esc_html_inline( 'Deactivate the free version', 'deactivate-free-version' ) . '.</li>' ) :
13636 '';
13637
13638 return sprintf(
13639 ' %s: <ol><li>%s.</li>%s<li>%s (<a href="%s" target="_blank">%s</a>).</li></ol>',
13640 $this->get_text_inline( 'Please follow these steps to complete the upgrade', 'follow-steps-to-complete-upgrade' ),
13641 $this->get_latest_download_link( sprintf(
13642 /* translators: %s: Plan title */
13643 $this->get_text_inline( 'Download the latest %s version', 'download-latest-x-version' ),
13644 $plan_title
13645 ) ),
13646 $deactivation_step,
13647 $this->get_text_inline( 'Upload and activate the downloaded version', 'upload-and-activate' ),
13648 '//bit.ly/upload-wp-' . $this->_module_type . 's',
13649 $this->get_text_inline( 'How to upload and activate?', 'howto-upload-activate' )
13650 );
13651 }
13652
13653 /**
13654 * @author Vova Feldman (@svovaf)
13655 * @since 1.2.1.7
13656 *
13657 * @param string $key
13658 *
13659 * @return string
13660 */
13661 function get_text( $key ) {
13662 return fs_text( $key, $this->_slug );
13663 }
13664
13665 /**
13666 * @author Vova Feldman (@svovaf)
13667 * @since 1.2.3
13668 *
13669 * @param string $text Translatable string.
13670 * @param string $key String key for overrides.
13671 *
13672 * @return string
13673 */
13674 function get_text_inline( $text, $key = '' ) {
13675 return _fs_text_inline( $text, $key, $this->_slug );
13676 }
13677
13678 /**
13679 * @author Vova Feldman (@svovaf)
13680 * @since 1.2.3
13681 *
13682 * @param string $text Translatable string.
13683 * @param string $context Context information for the translators.
13684 * @param string $key String key for overrides.
13685 *
13686 * @return string
13687 */
13688 function get_text_x_inline( $text, $context, $key ) {
13689 return _fs_text_x_inline( $text, $context, $key, $this->_slug );
13690 }
13691
13692 /**
13693 * @author Vova Feldman (@svovaf)
13694 * @since 1.2.3
13695 *
13696 * @param string $text Translatable string.
13697 * @param string $key String key for overrides.
13698 *
13699 * @return string
13700 */
13701 function esc_html_inline( $text, $key ) {
13702 return esc_html( _fs_text_inline( $text, $key, $this->_slug ) );
13703 }
13704
13705 #----------------------------------------------------------------------------------
13706 #region Versioning
13707 #----------------------------------------------------------------------------------
13708
13709 /**
13710 * Check if Freemius in SDK upgrade mode.
13711 *
13712 * @author Vova Feldman (@svovaf)
13713 * @since 1.0.9
13714 *
13715 * @return bool
13716 */
13717 function is_sdk_upgrade_mode() {
13718 return isset( $this->_storage->sdk_upgrade_mode ) ?
13719 $this->_storage->sdk_upgrade_mode :
13720 false;
13721 }
13722
13723 /**
13724 * Turn SDK upgrade mode off.
13725 *
13726 * @author Vova Feldman (@svovaf)
13727 * @since 1.0.9
13728 */
13729 function set_sdk_upgrade_complete() {
13730 $this->_storage->sdk_upgrade_mode = false;
13731 }
13732
13733 /**
13734 * Check if plugin upgrade mode.
13735 *
13736 * @author Vova Feldman (@svovaf)
13737 * @since 1.0.9
13738 *
13739 * @return bool
13740 */
13741 function is_plugin_upgrade_mode() {
13742 return isset( $this->_storage->plugin_upgrade_mode ) ?
13743 $this->_storage->plugin_upgrade_mode :
13744 false;
13745 }
13746
13747 /**
13748 * Turn plugin upgrade mode off.
13749 *
13750 * @author Vova Feldman (@svovaf)
13751 * @since 1.0.9
13752 */
13753 function set_plugin_upgrade_complete() {
13754 $this->_storage->plugin_upgrade_mode = false;
13755 }
13756
13757 #endregion
13758
13759 #----------------------------------------------------------------------------------
13760 #region Permissions
13761 #----------------------------------------------------------------------------------
13762
13763 /**
13764 * Check if specific permission requested.
13765 *
13766 * @author Vova Feldman (@svovaf)
13767 * @since 1.1.6
13768 *
13769 * @param string $permission
13770 *
13771 * @return bool
13772 */
13773 function is_permission_requested( $permission ) {
13774 return isset( $this->_permissions[ $permission ] ) && ( true === $this->_permissions[ $permission ] );
13775 }
13776
13777 #endregion
13778
13779 #----------------------------------------------------------------------------------
13780 #region Auto Activation
13781 #----------------------------------------------------------------------------------
13782
13783 /**
13784 * Hints the SDK if running an auto-installation.
13785 *
13786 * @var bool
13787 */
13788 private $_isAutoInstall = false;
13789
13790 /**
13791 * After upgrade callback to install and auto activate a plugin.
13792 * This code will only be executed on explicit request from the user,
13793 * following the practice Jetpack are using with their theme installations.
13794 *
13795 * @link https://make.wordpress.org/plugins/2017/03/16/clarification-of-guideline-8-executable-code-and-installs/
13796 *
13797 * @author Vova Feldman (@svovaf)
13798 * @since 1.2.1.7
13799 */
13800 function _install_premium_version_ajax_action() {
13801 $this->_logger->entrance();
13802
13803 $this->check_ajax_referer( 'install_premium_version' );
13804
13805 if ( ! $this->is_registered() ) {
13806 // Not registered.
13807 self::shoot_ajax_failure( array(
13808 'message' => $this->get_text_inline( 'Auto installation only works for opted-in users.', 'auto-install-error-not-opted-in' ),
13809 'code' => 'premium_installed',
13810 ) );
13811 }
13812
13813 $plugin_id = fs_request_get( 'target_module_id', $this->get_id() );
13814
13815 if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
13816 // Invalid ID.
13817 self::shoot_ajax_failure( array(
13818 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
13819 'code' => 'invalid_module_id',
13820 ) );
13821 }
13822
13823 if ( $plugin_id == $this->get_id() ) {
13824 if ( $this->is_premium() ) {
13825 // Already using the premium code version.
13826 self::shoot_ajax_failure( array(
13827 'message' => $this->get_text_inline( 'Premium version already active.', 'auto-install-error-premium-activated' ),
13828 'code' => 'premium_installed',
13829 ) );
13830 }
13831 if ( ! $this->can_use_premium_code() ) {
13832 // Don't have access to the premium code.
13833 self::shoot_ajax_failure( array(
13834 'message' => $this->get_text_inline( 'You do not have a valid license to access the premium version.', 'auto-install-error-invalid-license' ),
13835 'code' => 'invalid_license',
13836 ) );
13837 }
13838 if ( ! $this->has_release_on_freemius() ) {
13839 // Plugin is a serviceware, no premium code version.
13840 self::shoot_ajax_failure( array(
13841 'message' => $this->get_text_inline( 'Plugin is a "Serviceware" which means it does not have a premium code version.', 'auto-install-error-serviceware' ),
13842 'code' => 'premium_version_missing',
13843 ) );
13844 }
13845 } else {
13846 $addon = $this->get_addon( $plugin_id );
13847
13848 if ( ! is_object( $addon ) ) {
13849 // Invalid add-on ID.
13850 self::shoot_ajax_failure( array(
13851 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
13852 'code' => 'invalid_module_id',
13853 ) );
13854 }
13855
13856 if ( $this->is_addon_activated( $plugin_id, true ) ) {
13857 // Premium add-on version is already activated.
13858 self::shoot_ajax_failure( array(
13859 'message' => $this->get_text_inline( 'Premium add-on version already installed.', 'auto-install-error-premium-addon-activated' ),
13860 'code' => 'premium_installed',
13861 ) );
13862 }
13863 }
13864
13865 $this->_isAutoInstall = true;
13866
13867 // Try to install and activate.
13868 $updater = new FS_Plugin_Updater( $this );
13869 $result = $updater->install_and_activate_plugin( $plugin_id );
13870
13871 if ( is_array( $result ) && ! empty( $result['message'] ) ) {
13872 self::shoot_ajax_failure( array(
13873 'message' => $result['message'],
13874 'code' => $result['code'],
13875 ) );
13876 }
13877
13878 self::shoot_ajax_success( $result );
13879 }
13880
13881 /**
13882 * Displays module activation dialog box after a successful upgrade
13883 * where the user explicitly requested to auto download and install
13884 * the premium version.
13885 *
13886 * @author Vova Feldman (@svovaf)
13887 * @since 1.2.1.7
13888 */
13889 function _add_auto_installation_dialog_box() {
13890 $this->_logger->entrance();
13891
13892 if ( ! $this->is_registered() ) {
13893 // Not registered.
13894 return;
13895 }
13896
13897 $plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
13898
13899 if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
13900 // Invalid module ID.
13901 return;
13902 }
13903
13904 if ( $plugin_id == $this->get_id() ) {
13905 if ( $this->is_premium() ) {
13906 // Already using the premium code version.
13907 return;
13908 }
13909 if ( ! $this->can_use_premium_code() ) {
13910 // Don't have access to the premium code.
13911 return;
13912 }
13913 if ( ! $this->has_release_on_freemius() ) {
13914 // Plugin is a serviceware, no premium code version.
13915 return;
13916 }
13917 } else {
13918 $addon = $this->get_addon( $plugin_id );
13919
13920 if ( ! is_object( $addon ) ) {
13921 // Invalid add-on ID.
13922 return;
13923 }
13924
13925 if ( $this->is_addon_activated( $plugin_id, true ) ) {
13926 // Premium add-on version is already activated.
13927 return;
13928 }
13929 }
13930
13931 $vars = array(
13932 'id' => $this->_module_id,
13933 'target_module_id' => $plugin_id,
13934 'slug' => $this->_slug,
13935 );
13936
13937 fs_require_template( 'auto-installation.php', $vars );
13938 }
13939
13940 #endregion
13941
13942 #--------------------------------------------------------------------------------
13943 #region Tabs Integration
13944 #--------------------------------------------------------------------------------
13945
13946 #region Module's Original Tabs
13947
13948 /**
13949 * Inject a JavaScript logic to capture the theme tabs HTML.
13950 *
13951 * @author Vova Feldman (@svovaf)
13952 * @since 1.2.2.7
13953 */
13954 function _tabs_capture() {
13955 $this->_logger->entrance();
13956
13957 if ( ! $this->is_theme_settings_page() ||
13958 ! $this->is_matching_url( $this->main_menu_url() )
13959 ) {
13960 return;
13961 }
13962
13963 $params = array(
13964 'id' => $this->_module_id,
13965 );
13966
13967 fs_require_once_template( 'tabs-capture-js.php', $params );
13968 }
13969
13970 /**
13971 * Cache theme's tabs HTML for a week. The cache will also be set as expired
13972 * after version and type (free/premium) changes, in addition to the week period.
13973 *
13974 * @author Vova Feldman (@svovaf)
13975 * @since 1.2.2.7
13976 */
13977 function _store_tabs_ajax_action() {
13978 $this->_logger->entrance();
13979
13980 $this->check_ajax_referer( 'store_tabs' );
13981
13982 // Init filesystem if not yet initiated.
13983 WP_Filesystem();
13984
13985 // Get POST body HTML data.
13986 global $wp_filesystem;
13987 $tabs_html = $wp_filesystem->get_contents( "php://input" );
13988
13989 if ( is_string( $tabs_html ) ) {
13990 $tabs_html = trim( $tabs_html );
13991 }
13992
13993 if ( ! is_string( $tabs_html ) || empty( $tabs_html ) ) {
13994 self::shoot_ajax_failure();
13995 }
13996
13997 $this->_cache->set( 'tabs', $tabs_html, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
13998
13999 self::shoot_ajax_success();
14000 }
14001
14002 /**
14003 * Cache theme's settings page custom styles. The cache will also be set as expired
14004 * after version and type (free/premium) changes, in addition to the week period.
14005 *
14006 * @author Vova Feldman (@svovaf)
14007 * @since 1.2.2.7
14008 */
14009 function _store_tabs_styles() {
14010 $this->_logger->entrance();
14011
14012 if ( ! $this->is_theme_settings_page() ||
14013 ! $this->is_matching_url( $this->main_menu_url() )
14014 ) {
14015 return;
14016 }
14017
14018 $wp_styles = wp_styles();
14019
14020 $theme_styles_url = get_template_directory_uri();
14021
14022 $stylesheets = array();
14023 foreach ( $wp_styles->queue as $handler ) {
14024 if ( fs_starts_with( $handler, 'fs_' ) ) {
14025 // Assume that stylesheets that their handler starts with "fs_" belong to the SDK.
14026 continue;
14027 }
14028
14029 /**
14030 * @var _WP_Dependency $stylesheet
14031 */
14032 $stylesheet = $wp_styles->registered[ $handler ];
14033
14034 if ( fs_starts_with( $stylesheet->src, $theme_styles_url ) ) {
14035 $stylesheets[] = $stylesheet->src;
14036 }
14037 }
14038
14039 if ( ! empty( $stylesheets ) ) {
14040 $this->_cache->set( 'tabs_stylesheets', $stylesheets, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
14041 }
14042 }
14043
14044 /**
14045 * Check if module's original settings page has any tabs.
14046 *
14047 * @author Vova Feldman (@svovaf)
14048 * @since 1.2.2.7
14049 *
14050 * @return bool
14051 */
14052 private function has_tabs() {
14053 return $this->_cache->has( 'tabs' );
14054 }
14055
14056 /**
14057 * Get module's settings page HTML content, starting
14058 * from the beginning of the <div class="wrap"> element,
14059 * until the tabs HTML (including).
14060 *
14061 * @author Vova Feldman (@svovaf)
14062 * @since 1.2.2.7
14063 *
14064 * @return string
14065 */
14066 private function get_tabs_html() {
14067 $this->_logger->entrance();
14068
14069 return $this->_cache->get( 'tabs' );
14070 }
14071
14072 /**
14073 * Check if page should include tabs.
14074 *
14075 * @author Vova Feldman (@svovaf)
14076 * @since 1.2.2.7
14077 *
14078 * @return bool
14079 */
14080 private function should_page_include_tabs()
14081 {
14082 if ( ! $this->has_settings_menu() ) {
14083 // Don't add tabs if no settings at all.
14084 return false;
14085 }
14086
14087 if ( ! $this->is_theme() ) {
14088 // Only add tabs to themes for now.
14089 return false;
14090 }
14091
14092 if ( ! $this->has_paid_plan() && ! $this->has_addons() ) {
14093 // Only add tabs to monetizing themes.
14094 return false;
14095 }
14096
14097 if ( ! $this->is_theme_settings_page() ) {
14098 // Only add tabs if browsing one of the theme's setting pages.
14099 return false;
14100 }
14101
14102 if ( $this->is_admin_page( 'pricing' ) && fs_request_get_bool( 'checkout' ) ) {
14103 // Don't add tabs on checkout page, we want to reduce distractions
14104 // as much as possible.
14105 return false;
14106 }
14107
14108 return true;
14109 }
14110
14111 /**
14112 * Add the tabs HTML before the setting's page content and
14113 * enqueue any required stylesheets.
14114 *
14115 * @author Vova Feldman (@svovaf)
14116 * @since 1.2.2.7
14117 *
14118 * @return bool If tabs were included.
14119 */
14120 function _add_tabs_before_content() {
14121 $this->_logger->entrance();
14122
14123 if ( ! $this->should_page_include_tabs() ) {
14124 return false;
14125 }
14126
14127 /**
14128 * Enqueue the original stylesheets that are included in the
14129 * theme settings page. That way, if the theme settings has
14130 * some custom _styled_ content above the tabs UI, this
14131 * will make sure that the styling is preserved.
14132 */
14133 $stylesheets = $this->_cache->get( 'tabs_stylesheets', array() );
14134 if ( is_array( $stylesheets ) ) {
14135 for ( $i = 0, $len = count( $stylesheets ); $i < $len; $i ++ ) {
14136 wp_enqueue_style( "fs_{$this->_module_id}_tabs_{$i}", $stylesheets[ $i ] );
14137 }
14138 }
14139
14140 // Cut closing </div> tag.
14141 echo substr( trim( $this->get_tabs_html() ), 0, - 6 );
14142
14143 return true;
14144 }
14145
14146 /**
14147 * Add the tabs closing HTML after the setting's page content.
14148 *
14149 * @author Vova Feldman (@svovaf)
14150 * @since 1.2.2.7
14151 *
14152 * @return bool If tabs closing HTML was included.
14153 */
14154 function _add_tabs_after_content() {
14155 $this->_logger->entrance();
14156
14157 if ( ! $this->should_page_include_tabs() ) {
14158 return false;
14159 }
14160
14161 echo '</div>';
14162
14163 return true;
14164 }
14165
14166 #endregion
14167
14168 /**
14169 * Add in-page JavaScript to inject the Freemius tabs into
14170 * the module's setting tabs section.
14171 *
14172 * @author Vova Feldman (@svovaf)
14173 * @since 1.2.2.7
14174 */
14175 function _add_freemius_tabs() {
14176 $this->_logger->entrance();
14177
14178 if ( ! $this->should_page_include_tabs() ) {
14179 return;
14180 }
14181
14182 $params = array( 'id' => $this->_module_id );
14183 fs_require_once_template( 'tabs.php', $params );
14184 }
14185
14186 #endregion
14187
14188 #--------------------------------------------------------------------------------
14189 #region Customizer Integration for Themes
14190 #--------------------------------------------------------------------------------
14191
14192 /**
14193 * @author Vova Feldman (@svovaf)
14194 * @since 1.2.2.7
14195 *
14196 * @param WP_Customize_Manager $customizer
14197 */
14198 function _customizer_register($customizer) {
14199 $this->_logger->entrance();
14200
14201 if ( $this->is_pricing_page_visible() ) {
14202 require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-upsell-control.php';
14203
14204 $customizer->add_section( 'freemius_upsell', array(
14205 'title' => '&#9733; ' . $this->get_text_inline( 'View paid features', 'view-paid-features' ),
14206 'priority' => 1,
14207 ) );
14208 $customizer->add_setting( 'freemius_upsell', array(
14209 'sanitize_callback' => 'esc_html',
14210 ) );
14211
14212 $customizer->add_control( new FS_Customizer_Upsell_Control( $customizer, 'freemius_upsell', array(
14213 'fs' => $this,
14214 'section' => 'freemius_upsell',
14215 'priority' => 100,
14216 ) ) );
14217 }
14218
14219 if ( $this->is_page_visible( 'contact' ) || $this->is_page_visible( 'support' ) ) {
14220 require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-support-section.php';
14221
14222 // Main Documentation Link In Customizer Root.
14223 $customizer->add_section( new FS_Customizer_Support_Section( $customizer, 'freemius_support', array(
14224 'fs' => $this,
14225 'priority' => 1000,
14226 ) ) );
14227 }
14228 }
14229
14230 #endregion
14231
14232 /**
14233 * If the theme has a paid version, add some custom
14234 * styling to the theme's premium version (if exists)
14235 * to highlight that it's the premium version of the
14236 * same theme, making it easier for identification
14237 * after the user upgrades and upload it to the site.
14238 *
14239 * @author Vova Feldman (@svovaf)
14240 * @since 1.2.2.7
14241 */
14242 function _style_premium_theme() {
14243 $this->_logger->entrance();
14244
14245 if ( ! self::is_themes_page() ) {
14246 // Only include in the themes page.
14247 return;
14248 }
14249
14250 if ( ! $this->has_paid_plan() ) {
14251 // Only include if has any paid plans.
14252 return;
14253 }
14254
14255 $params = null;
14256 fs_require_once_template( '/js/jquery.content-change.php', $params );
14257
14258 $params = array(
14259 'slug' => $this->_slug,
14260 'id' => $this->_module_id,
14261 );
14262
14263 fs_require_template( '/js/style-premium-theme.php', $params );
14264 }
14265
14266 #----------------------------------------------------------------------------------
14267 #region Marketing
14268 #----------------------------------------------------------------------------------
14269
14270 /**
14271 * Check if current user purchased any other plugins before.
14272 *
14273 * @author Vova Feldman (@svovaf)
14274 * @since 1.0.9
14275 *
14276 * @return bool
14277 */
14278 function has_purchased_before() {
14279 // TODO: Implement has_purchased_before() method.
14280 throw new Exception( 'not implemented' );
14281 }
14282
14283 /**
14284 * Check if current user classified as an agency.
14285 *
14286 * @author Vova Feldman (@svovaf)
14287 * @since 1.0.9
14288 *
14289 * @return bool
14290 */
14291 function is_agency() {
14292 // TODO: Implement is_agency() method.
14293 throw new Exception( 'not implemented' );
14294 }
14295
14296 /**
14297 * Check if current user classified as a developer.
14298 *
14299 * @author Vova Feldman (@svovaf)
14300 * @since 1.0.9
14301 *
14302 * @return bool
14303 */
14304 function is_developer() {
14305 // TODO: Implement is_developer() method.
14306 throw new Exception( 'not implemented' );
14307 }
14308
14309 /**
14310 * Check if current user classified as a business.
14311 *
14312 * @author Vova Feldman (@svovaf)
14313 * @since 1.0.9
14314 *
14315 * @return bool
14316 */
14317 function is_business() {
14318 // TODO: Implement is_business() method.
14319 throw new Exception( 'not implemented' );
14320 }
14321
14322 #endregion
14323 }
14324