PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / trunk
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More vtrunk
6.35 6.34 6.33.1 6.33 6.32.1 6.32 6.31 6.25 6.25.1 6.26 6.26.1 6.27 6.28 6.29 6.3 6.3.1 6.3.2 6.30 6.4 6.4.1 6.4.2 6.5 6.5.1 6.5.2 6.5.3 All 141 releases
formidable / classes / models / FrmAddon.php

FrmAddon.php in Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More trunk, at classes/models/FrmAddon.php

1,436 lines 37.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if ( ! defined( 'ABSPATH' ) ) {
3 die( 'You are not allowed to call this page directly.' );
4 }
5
6 /** @phpstan-consistent-constructor */
7 class FrmAddon {
8
9 /**
10 * @var string
11 */
12 public $store_url = 'https://formidableforms.com';
13
14 /**
15 * @var int|string|null
16 */
17 public $download_id;
18
19 /**
20 * @var string|null
21 */
22 public $plugin_file;
23
24 /**
25 * @var string
26 */
27 public $plugin_folder;
28
29 /**
30 * @var string
31 */
32 public $plugin_name;
33
34 /**
35 * @var string
36 */
37 public $plugin_slug;
38
39 /**
40 * @var string
41 */
42 public $option_name;
43
44 /**
45 * @var string|null
46 */
47 public $version;
48
49 /**
50 * @var string
51 */
52 public $author = 'Strategy11';
53
54 /**
55 * @var bool
56 */
57 public $is_parent_licence = false;
58
59 /**
60 * @var bool
61 */
62 public $needs_license = true;
63
64 /**
65 * @var bool
66 */
67 private $is_expired_addon = false;
68
69 /**
70 * @var string|null
71 */
72 public $license;
73
74 /**
75 * @var bool
76 */
77 protected $get_beta = false;
78
79 /**
80 * @var array|null
81 */
82 protected $save_status;
83
84 /**
85 * This is used to decide whether the license checks should continue.
86 * The point is to avoid license issues when a site url changes.
87 *
88 * @since 6.8.3
89 *
90 * @var array
91 */
92 private $save_response = array();
93
94 /**
95 * This is used to flag other add ons not to send a request.
96 * We only want to send a single API request per page load.
97 *
98 * @since 6.8.3
99 *
100 * @var string
101 */
102 private $transient_lock_key = 'frm_activate_request_lock';
103
104 /**
105 * @since 6.8.3
106 *
107 * @var bool
108 */
109 protected $should_clear_cache = true;
110
111 /**
112 * Cached for the request, since every add-on asks for the same values.
113 *
114 * @since 6.35
115 *
116 * @var array<string,string>|null
117 */
118 private static $lite_requirements;
119
120 /**
121 * Add-ons that require more than Lite does, keyed by plugin slug.
122 *
123 * Nearly every add-on is plain PHP with no dependency of its own, so it runs anywhere Lite
124 * runs and Lite's own requirements describe it. The exceptions bundle a third party
125 * library, which sets a floor of its own. PDFs bundles Dompdf, which needs PHP 7.1.
126 *
127 * Values here are floors, never ceilings, so raising Lite's minimum still raises every
128 * add-on's. Keys match the names WordPress uses, requires and requires_php.
129 *
130 * @since 6.35
131 *
132 * @var array<string,array<string,string>>
133 */
134 private static $addon_requirements = array(
135 'pdfs' => array( 'requires_php' => '7.1' ),
136 );
137
138 public function __construct() {
139 if ( ! $this->plugin_slug ) {
140 $this->plugin_slug = preg_replace( '/[^a-zA-Z0-9_\s]/', '', str_replace( ' ', '_', strtolower( $this->plugin_name ) ) );
141 }
142
143 if ( ! $this->option_name ) {
144 $this->option_name = 'edd_' . $this->plugin_slug . '_license_';
145 }
146
147 $this->plugin_folder = plugin_basename( $this->plugin_file );
148 $this->license = $this->get_license();
149
150 add_filter( 'frm_installed_addons', array( &$this, 'insert_installed_addon' ) );
151 $this->edd_plugin_updater();
152 }
153
154 /**
155 * @return void
156 */
157 public static function load_hooks() {
158 add_filter( 'frm_include_addon_page', '__return_true' );
159 new static();
160 }
161
162 /**
163 * @param array $plugins
164 *
165 * @return array
166 */
167 public function insert_installed_addon( $plugins ) {
168 $plugins[ $this->plugin_slug ] = $this;
169 return $plugins;
170 }
171
172 /**
173 * @param string $plugin_slug
174 *
175 * @return false|FrmAddon
176 */
177 public static function get_addon( $plugin_slug ) {
178 $plugins = apply_filters( 'frm_installed_addons', array() );
179 return $plugins[ $plugin_slug ] ?? false;
180 }
181
182 /**
183 * @return void
184 */
185 public function edd_plugin_updater() {
186 $this->is_license_revoked();
187 $license = $this->license;
188
189 add_action( 'after_plugin_row_' . plugin_basename( $this->plugin_file ), array( $this, 'maybe_show_license_message' ), 10, 2 );
190
191 $is_addon = 'formidable/formidable.php' !== $this->plugin_folder;
192
193 if ( $is_addon ) {
194 // Lite gets its tested and required versions from wordpress.org, add-ons have no
195 // such source. Added before the license check, since a site without a license
196 // still needs to know what its add-ons support.
197 add_filter( 'site_transient_update_plugins', array( $this, 'add_version_requirements' ), 20 );
198 add_filter( 'plugins_api', array( $this, 'add_requirements_to_plugin_info' ), 20, 3 );
199 }
200
201 if ( ! $license ) {
202 return;
203 }
204
205 if ( $is_addon ) {
206 add_filter( 'plugins_api', array( &$this, 'plugins_api_filter' ), 10, 3 );
207 }
208
209 add_filter( 'site_transient_update_plugins', array( &$this, 'clear_expired_download' ) );
210 }
211
212 /**
213 * Updates information on the "View version 6.15 details" page with custom data.
214 *
215 * @uses api_request()
216 *
217 * @param mixed $_data
218 * @param string $_action
219 * @param object|null $_args
220 *
221 * @return object Data.
222 */
223 public function plugins_api_filter( $_data, $_action = '', $_args = null ) {
224 if ( $_action !== 'plugin_information' ) {
225 return $_data;
226 }
227
228 if ( empty( $_args->slug ) || ! $this->is_plugin_slug( $_args->slug ) ) {
229 return $_data;
230 }
231
232 $item_id = $this->download_id;
233
234 if ( $item_id ) {
235 $api = new FrmFormApi( $this->license );
236
237 // Force new API info so we can pull changelog data.
238 // Change log data is intentionally omitted from the cached API response
239 // To help reduce the size of the autoloaded option.
240 $api->force_api_request();
241 $plugins = $api->get_api_info();
242
243 if ( $plugins ) {
244 $_data = $plugins[ $item_id ];
245 }
246 }
247
248 if ( empty( $plugins ) ) {
249 $_data = array(
250 'name' => $this->plugin_name,
251 'excerpt' => '',
252 'changelog' => 'See the full changelog at <a href="' . esc_url( $this->store_url . '/changelog/' ) . '"></a>',
253 'banners' => array(
254 'high' => '',
255 'low' => 'https://ps.w.org/formidable/assets/banner-1544x500.png',
256 ),
257 );
258 }
259
260 $_data['sections'] = array(
261 'description' => $_data['excerpt'],
262 'changelog' => $_data['changelog'],
263 );
264 $_data['author'] = '<a href="' . esc_url( $this->store_url ) . '">' . esc_html( $this->author ) . '</a>';
265 $_data['homepage'] = $this->store_url;
266
267 return (object) $_data;
268 }
269
270 /**
271 * Checks if a requested plugin_information slug refers to this plugin.
272 *
273 * Either the plugin file name or the plugin folder may be used as an add-on's slug.
274 *
275 * @since 6.35
276 *
277 * @param string $slug The requested slug.
278 *
279 * @return bool
280 */
281 private function is_plugin_slug( $slug ) {
282 $file_slug = basename( $this->plugin_file, '.php' );
283 $folder_slug = str_replace( '/' . $file_slug . '.php', '', $this->plugin_folder );
284
285 return $slug === $file_slug || $slug === $folder_slug;
286 }
287
288 /**
289 * Adds the versions this add-on supports to its update data.
290 *
291 * The API reports no tested or required versions, so anything reading the update transient
292 * has no way to tell whether an add-on has been tried with the WordPress release in use, or
293 * whether the site meets what the add-on needs.
294 *
295 * @since 6.35
296 *
297 * @param mixed $transient The update_plugins site transient.
298 *
299 * @return mixed
300 */
301 public function add_version_requirements( $transient ) {
302 if ( ! is_object( $transient ) ) {
303 return $transient;
304 }
305
306 $requirements = $this->get_version_requirements();
307
308 if ( ! $requirements ) {
309 return $transient;
310 }
311
312 foreach ( array( 'response', 'no_update' ) as $group ) {
313 $plugins = $transient->$group ?? false;
314
315 if ( ! is_array( $plugins ) || empty( $plugins[ $this->plugin_folder ] ) ) {
316 continue;
317 }
318
319 // The entries are objects, so this updates the transient in place.
320 $plugin = $plugins[ $this->plugin_folder ];
321
322 if ( ! is_object( $plugin ) ) {
323 continue;
324 }
325
326 foreach ( $requirements as $key => $value ) {
327 if ( empty( $plugin->$key ) ) {
328 $plugin->$key = $value;
329 }
330 }
331 }
332
333 return $transient;
334 }
335
336 /**
337 * Adds the versions this add-on supports to its plugin information.
338 *
339 * This is where WordPress itself reads the values, for the "Compatible up to" and
340 * "Requires PHP" lines and the compatibility warnings in the plugin details modal. It is
341 * separate from plugins_api_filter because that filter is only added when a license is set.
342 *
343 * @since 6.35
344 *
345 * @param mixed $data Plugin information from an earlier filter, or false if none.
346 * @param string $action The requested action.
347 * @param object|null $args Arguments for the request, including the slug.
348 *
349 * @return mixed
350 */
351 public function add_requirements_to_plugin_info( $data, $action = '', $args = null ) {
352 if ( 'plugin_information' !== $action || empty( $args->slug ) || ! $this->is_plugin_slug( $args->slug ) ) {
353 return $data;
354 }
355
356 $requirements = $this->get_version_requirements();
357
358 if ( ! $requirements ) {
359 return $data;
360 }
361
362 $is_object = is_object( $data ) && ! is_wp_error( $data );
363
364 if ( ! $is_object && ! is_array( $data ) ) {
365 return $data;
366 }
367
368 foreach ( $requirements as $key => $value ) {
369 if ( ! $is_object ) {
370 if ( empty( $data[ $key ] ) ) {
371 $data[ $key ] = $value;
372 }
373 continue;
374 }
375
376 if ( empty( $data->$key ) ) {
377 $data->$key = $value;
378 }
379 }
380
381 return $data;
382 }
383
384 /**
385 * Gets the versions this add-on reports to WordPress.
386 *
387 * Lite's own values describe every add-on, apart from the few that bundle a library with a
388 * higher floor of its own.
389 *
390 * @since 6.35
391 *
392 * @return array<string,string> Keyed by the names WordPress uses, tested, requires and requires_php.
393 */
394 public function get_version_requirements() {
395 $requirements = self::get_lite_requirements();
396 $addon = self::$addon_requirements[ $this->plugin_slug ] ?? array();
397
398 foreach ( $addon as $key => $version ) {
399 $is_higher = empty( $requirements[ $key ] ) || version_compare( $version, $requirements[ $key ], '>' );
400
401 if ( $is_higher ) {
402 $requirements[ $key ] = $version;
403 }
404 }
405
406 /**
407 * Filters the versions a single add-on reports to WordPress.
408 *
409 * @since 6.35
410 *
411 * @param array<string,string> $requirements Keyed by tested, requires and requires_php.
412 * @param string $plugin_slug The add-on's slug, for example pdfs.
413 */
414 $requirements = apply_filters( 'frm_addon_version_requirements', $requirements, $this->plugin_slug );
415
416 return is_array( $requirements ) ? array_filter( $requirements ) : array();
417 }
418
419 /**
420 * Gets the WordPress version the add-ons report as tested.
421 *
422 * @since 6.35
423 *
424 * @return string The tested version, or an empty string if the readme has none.
425 */
426 public static function get_tested_wp_version() {
427 return self::get_lite_requirements()['tested'];
428 }
429
430 /**
431 * Gets the versions Lite declares, which the add-ons report as their own.
432 *
433 * Read from Lite's readme, which is the same place wordpress.org reads them from, so the
434 * add-ons report what Lite reports without anything being added to the API.
435 *
436 * @since 6.35
437 *
438 * @return array<string,string> Keyed by tested, requires and requires_php. Values may be empty.
439 */
440 public static function get_lite_requirements() {
441 if ( null !== self::$lite_requirements ) {
442 return self::$lite_requirements;
443 }
444
445 $headers = array(
446 'tested' => 'Tested up to',
447 'requires' => 'Requires at least',
448 'requires_php' => 'Requires PHP',
449 );
450
451 $versions = array(
452 'tested' => '',
453 'requires' => '',
454 'requires_php' => '',
455 );
456
457 $readme = FrmAppHelper::plugin_path() . '/readme.txt';
458
459 if ( is_readable( $readme ) ) {
460 $data = get_file_data( $readme, $headers );
461
462 foreach ( $versions as $key => $version ) {
463 $versions[ $key ] = isset( $data[ $key ] ) ? trim( $data[ $key ] ) : '';
464 }
465 }
466
467 /**
468 * Filters the versions read from Lite's readme before the add-ons report them.
469 *
470 * @since 6.35
471 *
472 * @param array<string,string> $versions Keyed by tested, requires and requires_php.
473 */
474 $versions = apply_filters( 'frm_lite_version_requirements', $versions );
475
476 if ( ! is_array( $versions ) ) {
477 $versions = array();
478 }
479
480 $versions['tested'] = self::expand_tested_branch( $versions['tested'] ?? '' );
481
482 self::$lite_requirements = $versions;
483
484 return self::$lite_requirements;
485 }
486
487 /**
488 * Expands a tested release branch to the release running on this site.
489 *
490 * A readme names a branch, like 7.0, while wordpress.org reports the newest release in
491 * that branch, like 7.0.4. The difference matters because WordPress compares the tested
492 * version to the current one exactly, so a site on 7.0.4 reading 7.0 is warned that the
493 * plugin is untested when it is not. Only versions within the tested branch are expanded,
494 * so a genuinely older tested version is still reported as older.
495 *
496 * @since 6.35
497 *
498 * @param string $tested The tested version from the readme.
499 *
500 * @return string
501 */
502 private static function expand_tested_branch( $tested ) {
503 if ( ! $tested ) {
504 return $tested;
505 }
506
507 // Drop any pre-release suffix, since 7.1-RC4 is not a release to report as tested.
508 $current = preg_replace( '/[^0-9.].*$/', '', get_bloginfo( 'version' ) );
509
510 if ( self::get_version_branch( $current ) !== self::get_version_branch( $tested ) ) {
511 return $tested;
512 }
513
514 return version_compare( $current, $tested, '>' ) ? $current : $tested;
515 }
516
517 /**
518 * Reduces a version to its major.minor release branch.
519 *
520 * @since 6.35
521 *
522 * @param string $version A WordPress version.
523 *
524 * @return string The branch, or an empty string if the version cannot be read.
525 */
526 private static function get_version_branch( $version ) {
527 if ( ! preg_match( '/^(\d+)\.(\d+)/', $version, $matches ) ) {
528 return '';
529 }
530
531 return $matches[1] . '.' . $matches[2];
532 }
533
534 /**
535 * @return string
536 */
537 public function get_license() {
538 $license = $this->maybe_get_pro_license();
539
540 if ( $license ) {
541 return $license;
542 }
543
544 $license = trim( get_option( $this->option_name . 'key' ) );
545
546 return $license ? $license : $this->activate_defined_license();
547 }
548
549 /**
550 * @since 3.04.03
551 *
552 * @return false|string
553 */
554 protected function maybe_get_pro_license() {
555 // Prevent a loop if $this is the pro plugin
556 $get_license = FrmAppHelper::pro_is_installed() && is_callable( 'FrmProAppHelper::get_updater' ) && $this->plugin_name !== 'Formidable Pro';
557
558 if ( ! $get_license ) {
559 return false;
560 }
561
562 $api = new FrmFormApi();
563 $api->get_pro_updater();
564 $license = $api->get_license();
565
566 if ( ! $license ) {
567 return false;
568 }
569
570 $this->get_api_info( $license );
571
572 return $this->is_parent_licence ? $license : false;
573 }
574
575 /**
576 * Activate the license in wp-config.php
577 *
578 * @since 2.04
579 *
580 * @return string
581 */
582 public function activate_defined_license() {
583 $license = $this->get_defined_license();
584
585 if ( $license && ! $this->is_active() && ! $this->checked_recently( '1 day' ) ) {
586 $response = $this->activate_license( $license );
587
588 if ( ! $response['success'] ) {
589 $license = '';
590 }
591 }
592
593 return $license;
594 }
595
596 /**
597 * Check the wp-config.php for the license key
598 *
599 * @since 2.04
600 *
601 * @return false|string
602 */
603 public function get_defined_license() {
604 $constant_name = 'FRM_' . strtoupper( $this->plugin_slug ) . '_LICENSE';
605 return defined( $constant_name ) ? constant( $constant_name ) : false;
606 }
607
608 /**
609 * @param string $license
610 *
611 * @return void
612 */
613 public function set_license( $license ) {
614 update_option( $this->option_name . 'key', $license );
615 }
616
617 /**
618 * @return mixed
619 */
620 public function is_active() {
621 return get_option( $this->option_name . 'active' );
622 }
623
624 /**
625 * @since 3.04.03
626 *
627 * @param array|string $error
628 *
629 * @return void
630 */
631 public function maybe_clear_license( $error ) {
632 if ( is_array( $error ) && $error['code'] === 'disabled' && $error['license'] === $this->license ) {
633 $this->clear_license();
634 }
635 }
636
637 /**
638 * @return void
639 */
640 public function clear_license() {
641 delete_option( $this->option_name . 'active' );
642 delete_option( $this->option_name . 'key' );
643
644 if ( ! $this->should_clear_cache ) {
645 return;
646 }
647
648 delete_site_option( $this->transient_key() );
649 delete_option( $this->transient_key() );
650 $this->delete_cache();
651 $this->should_clear_cache = true;
652 }
653
654 /**
655 * Don't save an invalid license.
656 *
657 * @since 6.8.3
658 *
659 * @param bool|string $is_valid If license activation was successful. May be a string 'valid'.
660 *
661 * @return void
662 */
663 protected function maybe_set_active( $is_valid ) {
664 update_option( $this->option_name . 'active', $is_valid );
665
666 if ( $is_valid ) {
667 $this->set_active( $is_valid );
668 return;
669 }
670
671 // Don't save the license if it's invalid.
672 $this->should_clear_cache = false;
673 $this->clear_license();
674 delete_option( $this->option_name . 'key' );
675 $this->license = '';
676 }
677
678 /**
679 * @param bool|string $is_active Value may include 'valid'.
680 *
681 * @return void
682 */
683 public function set_active( $is_active ) {
684 $this->delete_cache();
685 FrmAppHelper::save_combined_js();
686 $this->update_pro_capabilities();
687 }
688
689 /**
690 * Updates roles capabilities after pro license is active.
691 *
692 * @since 5.0
693 *
694 * @return void
695 */
696 protected function update_pro_capabilities() {
697 global $wp_roles;
698
699 if ( ! function_exists( 'get_editable_roles' ) ) {
700 require_once ABSPATH . 'wp-admin/includes/user.php';
701 }
702
703 $caps = FrmAppHelper::frm_capabilities( 'pro_only' );
704 $roles = get_editable_roles();
705 $settings = new FrmSettings();
706
707 foreach ( $caps as $cap => $cap_desc ) {
708 $cap_roles = (array) ( $settings->$cap ?? 'administrator' );
709
710 // Make sure administrators always have permissions.
711 if ( ! in_array( 'administrator', $cap_roles, true ) ) {
712 array_push( $cap_roles, 'administrator' );
713 }
714
715 foreach ( $roles as $role => $details ) {
716 if ( in_array( $role, $cap_roles, true ) ) {
717 $wp_roles->add_cap( $role, $cap );
718 } else {
719 $wp_roles->remove_cap( $role, $cap );
720 }
721 }
722 }
723 }
724
725 /**
726 * @since 3.04.03
727 *
728 * @return void
729 */
730 protected function delete_cache() {
731 delete_transient( 'frm_api_licence' );
732
733 // Cleanup option that has been removed.
734 delete_option( $this->option_name . 'last_activate' );
735
736 $api = new FrmFormApi( $this->license );
737 $api->reset_cached();
738
739 $api = new FrmFormTemplateApi( $this->license );
740 $api->reset_cached();
741
742 $api = new FrmApplicationApi( $this->license );
743 $api->reset_cached();
744
745 // The Sales API tries to pull API data in the constructor.
746 // But we do not need sales data here.
747 FrmSalesApi::prevent_new_sales_requests();
748
749 $api = new FrmSalesApi();
750 $api->reset_cached();
751
752 FrmSalesApi::allow_new_sales_requests();
753
754 $api = new FrmStyleApi();
755 $api->reset_cached();
756 }
757
758 /**
759 * The Pro version includes the show_license_message function.
760 * We need an extra check before we allow it to show a message.
761 *
762 * @since 3.04.03
763 *
764 * @param string $file File path of the plugin.
765 * @param array $plugin Plugin data.
766 *
767 * @return void
768 */
769 public function maybe_show_license_message( $file, $plugin ) {
770 if ( $this->is_expired_addon || isset( $plugin['package'] ) ) {
771 // Let's not show a ton of duplicate messages
772 return;
773 }
774
775 $this->show_license_message( $file, $plugin );
776 }
777
778 /**
779 * @param string $file File path of the plugin.
780 * @param array $plugin Plugin data.
781 *
782 * @return void
783 */
784 public function show_license_message( $file, $plugin ) {
785 $message = '';
786
787 if ( $this->license ) {
788 $api = new FrmFormApi( $this->license );
789 $errors = $api->error_for_license();
790
791 if ( $errors ) {
792 $message = reset( $errors );
793 }
794 } else {
795 /* translators: %1$s: Plugin name, %2$s: Start link HTML, %3$s: end link HTML */
796 $message = sprintf( esc_html__( 'Your %1$s license key is missing. Please add it on the %2$slicenses page%3$s.', 'formidable' ), esc_html( $this->plugin_name ), '<a href="' . esc_url( admin_url( 'admin.php?page=formidable-settings' ) ) . '">', '</a>' ); // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
797 }
798
799 if ( ! $message ) {
800 return;
801 }
802
803 $wp_list_table = _get_list_table( 'WP_Plugins_List_Table' );
804 $id = sanitize_title( $plugin['Name'] ) . '-next';
805
806 echo '<tr class="plugin-update-tr active" id="' . esc_attr( $id ) . '"><td colspan="' . esc_attr( $wp_list_table->get_column_count() ) . '" class="plugin-update colspanchange"><div class="update-message notice error inline notice-error notice-alt"><p>'; // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
807 FrmAppHelper::kses_echo( $message, 'a' );
808 echo '<script type="text/javascript">var d = document.getElementById("' . esc_attr( $id ) . '").previousSibling;if ( d !== null ){ d.className = d.className + " update"; }</script>'; // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
809 echo '</p></div></td></tr>';
810 }
811
812 /**
813 * @param mixed $transient
814 *
815 * @return mixed
816 */
817 public function clear_expired_download( $transient ) {
818 if ( ! is_object( $transient ) ) {
819 return $transient;
820 }
821
822 if ( $this->is_current_version( $transient ) ) {
823 // Make sure it doesn't show there is an update if plugin is up-to-date.
824 if ( isset( $transient->response[ $this->plugin_folder ] ) ) {
825 unset( $transient->response[ $this->plugin_folder ] );
826 }
827 } elseif ( isset( $transient->response ) && isset( $transient->response[ $this->plugin_folder ] ) ) {
828 $this->prepare_update_details( $transient->response[ $this->plugin_folder ] );
829
830 // If the transient has expired, clear the update and trigger it again
831 if ( $transient->response[ $this->plugin_folder ] === false ) {
832 if ( ! $this->has_been_cleared() ) {
833 $this->cleared_plugins();
834 $this->manually_queue_update();
835 }
836 unset( $transient->response[ $this->plugin_folder ] );
837 }
838 }
839
840 return $transient;
841 }
842
843 /**
844 * Check if the plugin information is correct to allow an update
845 *
846 * @since 3.04.03
847 *
848 * @param object $transient The current plugin info saved for update.
849 *
850 * @return void
851 */
852 private function prepare_update_details( &$transient ) {
853 $version_info = $transient;
854 $has_beta_url = ! empty( $version_info->beta );
855
856 if ( $this->get_beta && ! $has_beta_url ) {
857 $version_info = (object) $this->get_api_info( $this->license );
858 }
859
860 if ( empty( $version_info->new_version ) ) {
861 return;
862 }
863
864 $this->clear_old_plugin_version( $version_info );
865
866 if ( $version_info === false ) {
867 // Was cleared with timeout.
868 $transient = false;
869
870 return;
871 }
872
873 $this->maybe_use_beta_url( $version_info );
874
875 if ( version_compare( $version_info->new_version, $this->version, '>' ) ) {
876 $transient = $version_info;
877 }
878 }
879
880 /**
881 * Get the API info for this plugin
882 *
883 * @since 3.04.03
884 *
885 * @param string $license The license key.
886 *
887 * @return array The API info for the plugin.
888 */
889 protected function get_api_info( $license ) {
890 $api = new FrmFormApi( $license );
891 $addon = $api->get_addon_for_license( $this );
892
893 // If there is no download url, this license does not apply to the addon
894 if ( isset( $addon['package'] ) ) {
895 $this->is_parent_licence = true;
896 } elseif ( isset( $addon['error'] ) ) {
897 // If the license is expired, we must assume all add-ons were packaged
898 $this->is_parent_licence = true;
899 $this->is_expired_addon = true;
900 }
901
902 return $addon;
903 }
904
905 /**
906 * Make sure transients don't stick around on sites that
907 * don't save the transient expiration
908 *
909 * @since 2.05.05
910 *
911 * @param object $version_info The version info for the plugin.
912 *
913 * @return void
914 */
915 private function clear_old_plugin_version( &$version_info ) {
916 $timeout = ! empty( $version_info->timeout ) ? $version_info->timeout : 0;
917
918 if ( ! $timeout || time() <= $timeout ) {
919 return;
920 }
921
922 // Cache is expired.
923 $version_info = false;
924 $api = new FrmFormApi( $this->license );
925 $api->reset_cached();
926 }
927
928 /**
929 * The beta url is always included if the download has a beta.
930 * Check if the beta should be downloaded.
931 *
932 * @since 3.04.03
933 *
934 * @param object $version_info The version info for the plugin.
935 *
936 * @return void
937 */
938 private function maybe_use_beta_url( &$version_info ) {
939 if ( ! $this->get_beta || empty( $version_info->beta ) ) {
940 return;
941 }
942
943 $version_info->new_version = $version_info->beta['version'];
944 $version_info->package = $version_info->beta['package'];
945
946 if ( ! empty( $version_info->plugin ) ) {
947 $version_info->plugin = $version_info->beta['plugin'];
948 }
949 }
950
951 /**
952 * @param object $transient
953 *
954 * @return bool
955 */
956 private function is_current_version( $transient ) {
957 if ( empty( $transient->checked ) || ! isset( $transient->checked[ $this->plugin_folder ] ) ) {
958 return false;
959 }
960
961 $response = empty( $transient->response );
962
963 if ( $response ) {
964 return true;
965 }
966
967 return isset( $transient->response ) && isset( $transient->response[ $this->plugin_folder ] ) && $transient->checked[ $this->plugin_folder ] === $transient->response[ $this->plugin_folder ]->new_version; // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
968 }
969
970 /**
971 * @return bool
972 */
973 private function has_been_cleared() {
974 $last_cleared = get_option( 'frm_last_cleared' );
975 return $last_cleared && $last_cleared > gmdate( 'Y-m-d H:i:s', strtotime( '-5 minutes' ) );
976 }
977
978 /**
979 * @return void
980 */
981 private function cleared_plugins() {
982 update_option( 'frm_last_cleared', gmdate( 'Y-m-d H:i:s' ) );
983 }
984
985 /**
986 * @return void
987 */
988 private function is_license_revoked() {
989 if ( ! $this->license || ! $this->plugin_slug || isset( $_POST['license'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
990 return;
991 }
992
993 if ( $this->get_defined_license() ) {
994 // Don't check if the license is defined in wp-config.php since we can't remove it.
995 return;
996 }
997
998 // Only check weekly.
999 if ( $this->checked_recently( '7 days' ) || $this->is_running() ) {
1000 return;
1001 }
1002
1003 $response = $this->get_license_status();
1004
1005 if ( ! empty( $this->save_status['response_code'] ) && 429 === $this->save_status['response_code'] ) {
1006 // If we got a rate limit response, don't clear the license.
1007 return;
1008 }
1009
1010 if ( 'revoked' === $response['status'] || 'blocked' === $response['status'] || 'disabled' === $response['status'] || 'missing' === $response['status'] ) {
1011 $this->clear_license();
1012 }
1013 }
1014
1015 /**
1016 * Has this been checked too recently?
1017 *
1018 * @param string $time ie. '1 day'.
1019 *
1020 * @return bool True if the last check time was recent.
1021 */
1022 private function checked_recently( $time ) {
1023 $last_checked = $this->last_checked();
1024
1025 if ( ! $last_checked ) {
1026 return false;
1027 }
1028
1029 $is_429 = isset( $last_checked['response_code'] ) && 429 === $last_checked['response_code'];
1030
1031 // If the last check was a a rate limit error, adjust the check time.
1032 if ( $is_429 ) {
1033 // If the time is 2 minutes, do not adjust it so people
1034 // can still try to activate again in 2 minutes (and not 5).
1035 switch ( $time ) {
1036 case '7 days':
1037 // 7 days is used to check for license revocation.
1038 $time = '30 minutes';
1039 break;
1040 case '1 day':
1041 // 1 day is used for defined license constants.
1042 // The response won't change often, so we can check less frequently.
1043 $time = '2 hours';
1044 break;
1045 }
1046 }
1047
1048 if ( empty( $last_checked['time'] ) ) {
1049 // If we do not have time, data is really old.
1050 return false;
1051 }
1052
1053 $time_ago = gmdate( 'Y-m-d H:i:s', strtotime( '-' . $time ) );
1054 return $last_checked['time'] > $time_ago;
1055 }
1056
1057 /**
1058 * @since 6.8.3 Switched to an array to store extra response info.
1059 *
1060 * @return array
1061 */
1062 private function last_checked() {
1063 $last_checked = is_multisite() ? get_site_option( $this->transient_key() ) : get_option( $this->transient_key() );
1064
1065 if ( $last_checked && ! is_array( $last_checked ) ) {
1066 // Get string into array for existing values.
1067 $last_checked = array( 'time' => $last_checked );
1068 }
1069
1070 return $last_checked ? $last_checked : array();
1071 }
1072
1073 /**
1074 * @since 6.30 Added the $is_valid param.
1075 *
1076 * @param bool $is_valid
1077 *
1078 * @return void
1079 */
1080 private function update_last_checked( $is_valid ) {
1081 $this->save_response['time'] = gmdate( 'Y-m-d H:i:s' );
1082 $this->save_response['is_valid'] = $is_valid;
1083
1084 if ( is_multisite() ) {
1085 update_site_option( $this->transient_key(), $this->save_response );
1086 } else {
1087 update_option( $this->transient_key(), $this->save_response );
1088 }
1089 }
1090
1091 /**
1092 * Use a new cache after the license is changed, or Formidable is updated.
1093 *
1094 * @return string
1095 */
1096 private function transient_key() {
1097 return 'frm_' . md5( sanitize_key( $this->license . '_' . $this->plugin_slug ) );
1098 }
1099
1100 /**
1101 * @return void
1102 */
1103 public static function activate() {
1104 FrmAppHelper::permission_check( 'frm_change_settings' );
1105 check_ajax_referer( 'frm_ajax', 'nonce' );
1106
1107 $license = stripslashes( FrmAppHelper::get_param( 'license', '', 'post', 'sanitize_text_field' ) );
1108
1109 if ( ! $license ) {
1110 wp_send_json(
1111 array(
1112 'message' => __( 'Oops! You forgot to enter your license number.', 'formidable' ),
1113 'success' => false,
1114 )
1115 );
1116 }
1117
1118 $plugin_slug = FrmAppHelper::get_param( 'plugin', '', 'post', 'sanitize_text_field' );
1119 $response = self::activate_license_for_plugin( $license, $plugin_slug );
1120
1121 FrmInbox::clear_cache();
1122
1123 wp_send_json( $response );
1124 }
1125
1126 /**
1127 * @since 4.08
1128 *
1129 * @param string $license The license key.
1130 * @param string $plugin_slug The plugin slug.
1131 *
1132 * @return array The response from the license activation.
1133 */
1134 public static function activate_license_for_plugin( $license, $plugin_slug ) {
1135 $this_plugin = self::get_addon( $plugin_slug );
1136 return $this_plugin->activate_license( $license );
1137 }
1138
1139 /**
1140 * @param string $license The license key.
1141 *
1142 * @return array The response from the license activation.
1143 */
1144 private function activate_license( $license ) {
1145 $this->set_license( $license );
1146 $this->license = $license;
1147
1148 $this->die_if_not_allowed();
1149
1150 $response = $this->get_license_status();
1151 $response['message'] = '';
1152 $response['success'] = false;
1153
1154 $is_valid = false;
1155
1156 if ( $response['error'] ) {
1157 $response['message'] = $response['status'];
1158 } else {
1159 $messages = $this->get_messages();
1160
1161 if ( is_string( $response['status'] ) && isset( $messages[ $response['status'] ] ) ) {
1162 $response['message'] = $messages[ $response['status'] ];
1163 } else {
1164 $response['message'] = FrmAppHelper::kses( $response['status'], array( 'a' ) );
1165 }
1166
1167 if ( 'valid' === $response['status'] ) {
1168 $is_valid = 'valid';
1169 $response['success'] = true;
1170 }
1171
1172 $this->maybe_set_active( $is_valid );
1173 }
1174
1175 $this->update_last_checked( (bool) $is_valid );
1176
1177 return $response;
1178 }
1179
1180 /**
1181 * Prevent this check from happening more than once per minute with the same license.
1182 *
1183 * @return void
1184 */
1185 private function die_if_not_allowed() {
1186 if ( ! $this->checked_recently( '2 minutes' ) ) {
1187 return;
1188 }
1189
1190 // Don't check more than once per minute.
1191 wp_send_json(
1192 array(
1193 'message' => __( 'Please wait two minutes before trying again.', 'formidable' ),
1194 'success' => false,
1195 )
1196 );
1197 }
1198
1199 /**
1200 * @return array
1201 */
1202 private function get_license_status() {
1203 $this->set_running();
1204
1205 $response = array(
1206 'status' => 'missing',
1207 'error' => true,
1208 );
1209
1210 if ( ! $this->license ) {
1211 $response['error'] = false;
1212 return $response;
1213 }
1214
1215 $is_valid = false;
1216
1217 try {
1218 $response['error'] = false;
1219 $license_data = $this->send_mothership_request( 'activate_license' );
1220
1221 // $license_data->license will be either "valid" or "invalid"
1222 if ( is_array( $license_data ) ) {
1223 if ( ! empty( $license_data['license'] ) && in_array( $license_data['license'], array( 'valid', 'invalid' ), true ) ) {
1224 $response['status'] = $license_data['license'];
1225 $this->save_status['status'] = $license_data['license'];
1226 $is_valid = 'valid' === $license_data['license'];
1227 }
1228 } else {
1229 $response['status'] = $license_data;
1230 }
1231 } catch ( Exception $e ) {
1232 $response['status'] = $e->getMessage();
1233 }
1234
1235 $this->update_last_checked( $is_valid );
1236 $this->done_running();
1237 return $response;
1238 }
1239
1240 /**
1241 * @return array
1242 */
1243 private function get_messages() {
1244 return array(
1245 'valid' => __( 'Your license has been activated. Enjoy!', 'formidable' ),
1246 'invalid' => __( 'That license key is invalid', 'formidable' ),
1247 'expired' => __( 'That license is expired', 'formidable' ),
1248 'revoked' => __( 'That license has been refunded', 'formidable' ),
1249 'no_activations_left' => __( 'That license has been used on too many sites', 'formidable' ),
1250 'invalid_item_id' => __( 'Oops! That is the wrong license key for this plugin.', 'formidable' ),
1251 'missing' => __( 'That license key is invalid', 'formidable' ),
1252 );
1253 }
1254
1255 /**
1256 * @since 4.03
1257 *
1258 * @return void
1259 */
1260 public static function reset_cache() {
1261 FrmAppHelper::permission_check( 'frm_change_settings' );
1262 check_ajax_referer( 'frm_ajax', 'nonce' );
1263
1264 $this_plugin = self::set_license_from_post();
1265 $this_plugin->delete_cache();
1266
1267 $response = array(
1268 'success' => true,
1269 'message' => __( 'Cache cleared', 'formidable' ),
1270 );
1271
1272 wp_send_json( $response );
1273 }
1274
1275 /**
1276 * @return void
1277 */
1278 public static function deactivate() {
1279 FrmAppHelper::permission_check( 'frm_change_settings' );
1280 check_ajax_referer( 'frm_ajax', 'nonce' );
1281
1282 $this_plugin = self::set_license_from_post();
1283
1284 $response = array(
1285 'success' => false,
1286 'message' => '',
1287 );
1288
1289 try {
1290 // $license_data->license will be either "deactivated" or "failed"
1291 $license_data = $this_plugin->send_mothership_request( 'deactivate_license' );
1292
1293 if ( is_array( $license_data ) && 'deactivated' === $license_data['license'] ) {
1294 $response['success'] = true;
1295 $response['message'] = __( 'That license was removed successfully', 'formidable' );
1296 } else {
1297 $response['message'] = __( 'There was an error deactivating your license.', 'formidable' );
1298 }
1299 } catch ( Exception $e ) {
1300 $response['message'] = $e->getMessage();
1301 }
1302
1303 $this_plugin->clear_license();
1304 FrmInbox::clear_cache();
1305
1306 wp_send_json( $response );
1307 }
1308
1309 /**
1310 * @since 4.03
1311 *
1312 * @return FrmAddon
1313 */
1314 private static function set_license_from_post() {
1315 $plugin_slug = FrmAppHelper::get_param( 'plugin', '', 'post', 'sanitize_text_field' );
1316 $this_plugin = self::get_addon( $plugin_slug );
1317 $this_plugin->license = $this_plugin->get_license();
1318 return $this_plugin;
1319 }
1320
1321 /**
1322 * @param string $action
1323 *
1324 * @return string
1325 */
1326 public function send_mothership_request( $action ) {
1327 $api_params = array(
1328 'edd_action' => $action,
1329 'license' => $this->license,
1330 'url' => home_url(),
1331 );
1332
1333 if ( is_numeric( $this->download_id ) ) {
1334 $api_params['item_id'] = absint( $this->download_id );
1335 } else {
1336 $api_params['item_name'] = rawurlencode( $this->plugin_name );
1337 }
1338
1339 $arg_array = array(
1340 'body' => $api_params,
1341 'timeout' => 25,
1342 'user-agent' => $this->plugin_slug . '/' . $this->version . '; ' . get_bloginfo( 'url' ),
1343 );
1344
1345 $resp = wp_remote_post(
1346 $this->store_url . '?l=' . urlencode( base64_encode( $this->license ) ),
1347 $arg_array
1348 );
1349 $response_code = wp_remote_retrieve_response_code( $resp );
1350 $body = wp_remote_retrieve_body( $resp );
1351 $this->save_status = array( 'response_code' => $response_code );
1352 $this->save_response['response_code'] = $response_code;
1353
1354 $message = __( 'Your License Key was invalid', 'formidable' );
1355
1356 if ( is_wp_error( $resp ) ) {
1357 $link = FrmAppHelper::admin_upgrade_link( 'api', 'knowledgebase/why-cant-i-activate-formidable-pro/' );
1358 /* translators: %1$s: Start link HTML, %2$s: End link HTML */
1359 $message = sprintf( __( 'You had an error communicating with the Formidable API. %1$sClick here%2$s for more information.', 'formidable' ), '<a href="' . esc_url( $link ) . '" target="_blank">', '</a>' ); // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
1360 $message .= ' ' . $resp->get_error_message();
1361 } elseif ( 'error' === $body || is_wp_error( $body ) ) {
1362 $message = __( 'You had an HTTP error connecting to the Formidable API', 'formidable' );
1363 } else {
1364 $json_res = json_decode( $body, true );
1365
1366 if ( null !== $json_res ) {
1367 $message = is_array( $json_res ) && isset( $json_res['error'] ) ? $json_res['error'] : $json_res;
1368 } elseif ( ! empty( $resp['response'] ) && ! empty( $resp['response']['code'] ) ) {
1369 $resp['body'] = wp_strip_all_tags( $resp['body'] );
1370
1371 $message = sprintf(
1372 /* translators: %1$s: Error code, %2$s: Error message */
1373 esc_html__( 'There was a %1$s error: %2$s', 'formidable' ),
1374 esc_html( $resp['response']['code'] ),
1375 $resp['response']['message'] . ' ' . $resp['body']
1376 );
1377 }
1378 }//end if
1379
1380 return $message;
1381 }
1382
1383 /**
1384 * @return void
1385 */
1386 public function manually_queue_update() {
1387 $updates = new stdClass();
1388 $updates->last_checked = 0;
1389 $updates->response = array();
1390 $updates->translations = array();
1391 $updates->no_update = array();
1392 $updates->checked = array();
1393 set_site_transient( 'update_plugins', $updates );
1394 }
1395
1396 /**
1397 * Set the transient key for the lock. It should be unique to the license.
1398 *
1399 * @since 6.8.3
1400 *
1401 * @return bool
1402 */
1403 protected function lock_key() {
1404 return $this->transient_lock_key . '_' . $this->license;
1405 }
1406
1407 /**
1408 * Prevent multiple requests from running at the same time.
1409 *
1410 * @since 6.8.3
1411 *
1412 * @return bool
1413 */
1414 protected function is_running() {
1415 return get_transient( $this->lock_key() );
1416 }
1417
1418 /**
1419 * @since 6.8.3
1420 *
1421 * @return void
1422 */
1423 protected function set_running() {
1424 set_transient( $this->lock_key(), true, 2 * MINUTE_IN_SECONDS );
1425 }
1426
1427 /**
1428 * @since 6.8.3
1429 *
1430 * @return void
1431 */
1432 protected function done_running() {
1433 delete_transient( $this->lock_key() );
1434 }
1435 }
1436