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 / FrmFormApi.php

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

580 lines 12.2 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 class FrmFormApi {
7
8 /**
9 * @var string
10 */
11 protected $license = '';
12
13 /**
14 * @var string
15 */
16 protected $cache_key = '';
17
18 /**
19 * @var string
20 */
21 protected $cache_timeout = '+6 hours';
22
23 /**
24 * The number of days an add-on is new.
25 *
26 * @var int
27 */
28 protected $new_days = 90;
29
30 /**
31 * If true, calls to get_api_info will bypass cache.
32 * This is set true by calling force_api_request.
33 *
34 * @var bool
35 */
36 protected $force = false;
37
38 /**
39 * The most recent decode of each cache key's payload.
40 *
41 * Each entry is array( 'raw' => string, 'decoded' => mixed ). The raw JSON is kept so a reuse
42 * can be checked against what is stored right now, rather than assumed to still be current.
43 *
44 * @var array<string,array>
45 */
46 private static $decoded_cache = array();
47
48 /**
49 * @since 3.06
50 *
51 * @param string|null $license The license key.
52 *
53 * @return void
54 */
55 public function __construct( $license = null ) {
56 $this->set_license( $license );
57 $this->set_cache_key();
58 }
59
60 /**
61 * @since 3.06
62 *
63 * @param string|null $license The license key.
64 *
65 * @return void
66 */
67 private function set_license( $license ) {
68 if ( $license === null ) {
69 $edd_update = $this->get_pro_updater();
70
71 if ( $edd_update ) {
72 $license = $edd_update->license;
73 }
74 }
75 $this->license = $license;
76 }
77
78 /**
79 * @since 3.06
80 *
81 * @return string
82 */
83 public function get_license() {
84 return $this->license;
85 }
86
87 /**
88 * @since 3.06
89 *
90 * @return void
91 */
92 protected function set_cache_key() {
93 $this->cache_key = 'frm_addons_l' . ( $this->license ? md5( $this->license ) : '' );
94 }
95
96 /**
97 * @since 3.06
98 *
99 * @return string
100 */
101 public function get_cache_key() {
102 return $this->cache_key;
103 }
104
105 /**
106 * Flag the force property as true, so the next API request bypasses cache.
107 * This is used to pull API data for change logs, which are excluded from the cached data.
108 *
109 * @since 6.28
110 *
111 * @return void
112 */
113 public function force_api_request() {
114 $this->force = true;
115 }
116
117 /**
118 * @since 3.06
119 *
120 * @return array
121 */
122 public function get_api_info() {
123 $url = $this->api_url();
124
125 if ( $this->license ) {
126 $url .= '?l=' . urlencode( base64_encode( $this->license ) );
127 }
128
129 if ( $this->force ) {
130 $addons = false;
131 $this->force = false;
132 } else {
133 $addons = $this->get_cached();
134 }
135
136 if ( is_array( $addons ) ) {
137 return $addons;
138 }
139
140 if ( $this->is_running() ) {
141 // If there's no saved cache, we'll need to wait for the current request to finish.
142 return array();
143 }
144
145 $this->set_running();
146
147 // We need to know the version number to allow different downloads.
148 $agent = 'formidable/' . FrmAppHelper::plugin_version();
149
150 if ( class_exists( 'FrmProDb' ) ) {
151 $agent = 'formidable-pro/' . FrmProDb::$plug_version;
152 }
153
154 $response = wp_remote_get(
155 $url,
156 array(
157 'timeout' => 25,
158 'user-agent' => $agent . '; ' . get_bloginfo( 'url' ),
159 )
160 );
161
162 if ( is_array( $response ) && ! is_wp_error( $response ) ) {
163 $addons = $response['body'] ? json_decode( $response['body'], true ) : array();
164 }
165
166 if ( ! is_array( $addons ) ) {
167 $addons = array();
168 }
169
170 $addons['response_code'] = wp_remote_retrieve_response_code( $response );
171
172 foreach ( $addons as $k => $addon ) {
173 if ( ! is_array( $addon ) ) {
174 continue;
175 }
176
177 if ( isset( $addon['categories'] ) ) {
178 $cats = array_intersect( $this->skip_categories(), $addon['categories'] );
179
180 if ( $cats ) {
181 unset( $addons[ $k ] );
182 continue;
183 }
184 }
185
186 if ( ! array_key_exists( 'is_new', $addon ) && array_key_exists( 'released', $addon ) ) {
187 $addons[ $k ]['is_new'] = $this->is_new( $addon );
188 }
189 }
190
191 $this->set_cached( $addons );
192 $this->done_running();
193
194 return $addons;
195 }
196
197 /**
198 * Prevent multiple requests from running at the same time.
199 *
200 * @since 6.8.3
201 *
202 * @return bool
203 */
204 protected function is_running() {
205 if ( $this->run_as_multisite() ) {
206 return get_site_transient( $this->transient_key() );
207 }
208 return get_transient( $this->transient_key() );
209 }
210
211 /**
212 * @since 6.8.3
213 *
214 * @return void
215 */
216 protected function set_running() {
217 $expires = 2 * MINUTE_IN_SECONDS;
218
219 if ( $this->run_as_multisite() ) {
220 set_site_transient( $this->transient_key(), true, $expires );
221 return;
222 }
223
224 set_transient( $this->transient_key(), true, $expires );
225 }
226
227 /**
228 * @since 6.8.3
229 *
230 * @return void
231 */
232 protected function done_running() {
233 if ( $this->run_as_multisite() ) {
234 delete_site_transient( $this->transient_key() );
235 }
236 delete_transient( $this->transient_key() );
237 }
238
239 /**
240 * Only allow one site in the network to make the api request at a time.
241 * If there is a license for the request, run individually.
242 *
243 * @since 6.8.3
244 *
245 * @return bool
246 */
247 protected function run_as_multisite() {
248 return is_multisite() && ! $this->license;
249 }
250
251 /**
252 * @since 6.8.3
253 *
254 * @return string
255 */
256 protected function transient_key() {
257 return strtolower( static::class ) . '_request_lock';
258 }
259
260 /**
261 * @since 3.06
262 *
263 * @return string
264 */
265 protected function api_url() {
266 if ( ! $this->license ) {
267 // Direct traffic to Cloudflare worker when there is no license.
268 return 'https://plapi.formidableforms.com/list/';
269 }
270 return 'https://formidableforms.com/wp-json/s11edd/v1/updates/';
271 }
272
273 /**
274 * @since 3.06
275 *
276 * @return string[]
277 */
278 protected function skip_categories() {
279 return array( 'WordPress Form Templates', 'WordPress Form Style Templates' );
280 }
281
282 /**
283 * @since 3.06
284 *
285 * @param object $license_plugin The FrmAddon object.
286 * @param array $addons
287 *
288 * @return array
289 */
290 public function get_addon_for_license( $license_plugin, $addons = array() ) {
291 if ( ! $addons ) {
292 $addons = $this->get_api_info();
293 }
294
295 $download_id = $license_plugin->download_id;
296 $plugin = array();
297
298 if ( ! $download_id && $addons ) {
299 foreach ( $addons as $addon ) {
300 if ( is_array( $addon ) && ! empty( $addon['title'] ) && 0 === strcasecmp( $license_plugin->plugin_name, $addon['title'] ) ) {
301 return $addon;
302 }
303 }
304 } elseif ( isset( $addons[ $download_id ] ) ) {
305 $plugin = $addons[ $download_id ];
306 }
307
308 return $plugin;
309 }
310
311 /**
312 * @since 3.06
313 *
314 * @return false|object
315 */
316 public function get_pro_updater() {
317 if ( FrmAppHelper::pro_is_installed() && is_callable( 'FrmProAppHelper::get_updater' ) ) {
318 $updater = FrmProAppHelper::get_updater();
319 $this->set_license( $updater->license );
320
321 return $updater;
322 }
323
324 return false;
325 }
326
327 /**
328 * @since 3.06
329 *
330 * @return array|bool
331 */
332 protected function get_cached() {
333 $cache = $this->get_cached_option();
334
335 if ( ! $cache ) {
336 return false;
337 }
338
339 $is_expired = empty( $cache['timeout'] ) || time() > $cache['timeout'];
340
341 if ( ! $is_expired && isset( $cache['version'] ) && $cache['version'] !== FrmAppHelper::plugin_version() ) {
342 $is_expired = true;
343 }
344
345 // Avoid old cached data, unless we're currently trying to query for new data.
346 // The call to $this->is_running likely triggers a database query, so only call if if we're expired.
347 // (Rather than the other way around, which is less efficient).
348 if ( $is_expired && ! $this->is_running() ) {
349 return false;
350 }
351
352 return $this->decode_cached( $cache['value'] );
353 }
354
355 /**
356 * Decode a cached API payload, reusing the previous decode of an unchanged payload.
357 *
358 * The payload runs to tens of kilobytes of JSON, and decoding it costs far more than reading
359 * the option it came from. That matters because a single form builder page decodes it once per
360 * upsell it renders. The reuse is keyed on the raw JSON rather than on the cache key, so
361 * anything that changes what is stored -- a new license, a cleared cache, a fresh API response,
362 * a test replacing the option -- is picked up on the very next read instead of being masked.
363 *
364 * @since 6.35
365 *
366 * @param string $value The cached JSON.
367 *
368 * @return mixed
369 */
370 private function decode_cached( $value ) {
371 $previous = self::$decoded_cache[ $this->cache_key ] ?? false;
372
373 if ( $previous && $previous['raw'] === $value ) {
374 return $previous['decoded'];
375 }
376
377 $decoded = json_decode( $value, true );
378
379 self::$decoded_cache[ $this->cache_key ] = array(
380 'raw' => $value,
381 'decoded' => $decoded,
382 );
383
384 return $decoded;
385 }
386
387 /**
388 * Get the cache for the network if multisite.
389 *
390 * @since 6.8.3
391 *
392 * @return mixed
393 */
394 protected function get_cached_option() {
395 if ( is_multisite() ) {
396 $cached = get_site_option( $this->cache_key );
397
398 if ( $cached ) {
399 return $cached;
400 }
401 }
402
403 return get_option( $this->cache_key );
404 }
405
406 /**
407 * @since 3.06
408 *
409 * @param array $addons
410 *
411 * @return void
412 */
413 protected function set_cached( $addons ) {
414 $addons = $this->reduce_addon_data_before_caching( $addons );
415
416 $data = array(
417 'timeout' => strtotime( $this->get_cache_timeout( $addons ), time() ),
418 'value' => wp_json_encode( $addons ),
419 'version' => FrmAppHelper::plugin_version(),
420 );
421
422 if ( is_multisite() ) {
423 update_site_option( $this->cache_key, $data );
424 } else {
425 // Autoload the license cache because it gets called everywhere.
426 $autoload = str_starts_with( $this->cache_key, 'frm_addons_l' );
427 update_option( $this->cache_key, $data, $autoload );
428 }
429 }
430
431 /**
432 * Remove certain add-on API data that we don't need to cache.
433 * This is to help keep the option data (which is auto-loaded) small.
434 *
435 * @since 6.28
436 *
437 * @param array $addons
438 *
439 * @return array
440 */
441 private function reduce_addon_data_before_caching( $addons ) {
442 if ( is_subclass_of( $this, 'FrmFormApi' ) ) {
443 // We only want to modify FrmFormApi. Leave the other APIs alone for now.
444 return $addons;
445 }
446
447 $reduced_addons = array();
448
449 foreach ( $addons as $key => $addon ) {
450 if ( ! is_array( $addon ) ) {
451 $reduced_addons[ $key ] = $addon;
452 continue;
453 }
454
455 if ( ! $this->should_include_addon_in_cached_data( $addon ) ) {
456 continue;
457 }
458
459 if ( isset( $addon['changelog'] ) ) {
460 unset( $addon['changelog'], $addon['banners'] );
461 }
462
463 $reduced_addons[ $key ] = $addon;
464 }
465
466 return $reduced_addons;
467 }
468
469 /**
470 * @since 6.28
471 *
472 * @param array $addon
473 *
474 * @return bool True if the add-on should be included in cached data.
475 */
476 private function should_include_addon_in_cached_data( $addon ) {
477 if ( isset( $addon['version'] ) && '' === $addon['version'] ) {
478 // If version is set but blank, the plugin is not actually live.
479 return false;
480 }
481
482 if ( isset( $addon['categories'] ) ) {
483 if ( isset( $addon['slug'] ) && 'views' === $addon['slug'] ) {
484 // Legacy views has no categories set, but we should still
485 // Include it in cache since it is a valid add-on.
486 return true;
487 }
488
489 $categories_are_empty = ! $addon['categories'] || $addon['categories'] === array( 'Strategy11' );
490
491 if ( $categories_are_empty ) {
492 return false;
493 }
494 }
495
496 return true;
497 }
498
499 /**
500 * If the last check was a a rate limit, we'll need to check again sooner.
501 *
502 * @since 6.8.3
503 *
504 * @param array $addons
505 *
506 * @return string
507 */
508 protected function get_cache_timeout( $addons ) {
509 if ( isset( $addons['response_code'] ) && 429 === $addons['response_code'] ) {
510 return '+5 minutes';
511 }
512 return $this->cache_timeout;
513 }
514
515 /**
516 * @since 3.06
517 *
518 * @return void
519 */
520 public function reset_cached() {
521 if ( is_multisite() ) {
522 delete_site_option( $this->cache_key );
523 } else {
524 delete_option( $this->cache_key );
525 }
526 $this->done_running();
527 }
528
529 /**
530 * @since 3.06
531 *
532 * @return array
533 */
534 public function error_for_license() {
535 return $this->license ? $this->get_error_from_response() : array();
536 }
537
538 /**
539 * @since 3.06
540 *
541 * @param array $addons
542 *
543 * @return array
544 */
545 public function get_error_from_response( $addons = array() ) {
546 if ( ! $addons ) {
547 $addons = $this->get_api_info();
548 }
549
550 $errors = array();
551
552 if ( ! isset( $addons['error'] ) ) {
553 return $errors;
554 }
555
556 if ( is_string( $addons['error'] ) ) {
557 $errors[] = $addons['error'];
558 } elseif ( ! empty( $addons['error']['message'] ) ) {
559 $errors[] = $addons['error']['message'];
560 }
561
562 do_action( 'frm_license_error', $addons['error'] );
563
564 return $errors;
565 }
566
567 /**
568 * Check if a template is new.
569 *
570 * @since 6.0
571 *
572 * @param array $addon
573 *
574 * @return bool
575 */
576 protected function is_new( $addon ) {
577 return strtotime( $addon['released'] ) > strtotime( '-' . $this->new_days . ' days' );
578 }
579 }
580