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
← All changes | classes/models/FrmFormApi.php +203 -44 6.25 → trunk View file →
@@ -27,9 +27,31 @@
27 27 */
28 28 protected $new_days = 90;
29 29
30 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 + /**
31 49 * @since 3.06
50 + *
51 + * @param string|null $license The license key.
52 + *
53 + * @return void
32 54 */
33 55 public function __construct( $license = null ) {
34 56 $this->set_license( $license );
35 57 $this->set_cache_key();
@@ -37,14 +59,17 @@
37 59
38 60 /**
39 61 * @since 3.06
40 62 *
63 + * @param string|null $license The license key.
64 + *
41 65 * @return void
42 66 */
43 67 private function set_license( $license ) {
44 68 if ( $license === null ) {
45 69 $edd_update = $this->get_pro_updater();
46 - if ( ! empty( $edd_update ) ) {
70 +
71 + if ( $edd_update ) {
47 72 $license = $edd_update->license;
48 73 }
49 74 }
50 75 $this->license = $license;
@@ -51,8 +76,9 @@
51 76 }
52 77
53 78 /**
54 79 * @since 3.06
80 + *
55 81 * @return string
56 82 */
57 83 public function get_license() {
58 84 return $this->license;
@@ -63,13 +89,14 @@
63 89 *
64 90 * @return void
65 91 */
66 92 protected function set_cache_key() {
67 - $this->cache_key = 'frm_addons_l' . ( empty( $this->license ) ? '' : md5( $this->license ) );
93 + $this->cache_key = 'frm_addons_l' . ( $this->license ? md5( $this->license ) : '' );
68 94 }
69 95
70 96 /**
71 97 * @since 3.06
98 + *
72 99 * @return string
73 100 */
74 101 public function get_cache_key() {
75 102 return $this->cache_key;
@@ -75,18 +102,38 @@
75 102 return $this->cache_key;
76 103 }
77 104
78 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 + /**
79 118 * @since 3.06
119 + *
80 120 * @return array
81 121 */
82 122 public function get_api_info() {
83 123 $url = $this->api_url();
84 - if ( ! empty( $this->license ) ) {
124 +
125 + if ( $this->license ) {
85 126 $url .= '?l=' . urlencode( base64_encode( $this->license ) );
86 127 }
87 128
88 - $addons = $this->get_cached();
129 + if ( $this->force ) {
130 + $addons = false;
131 + $this->force = false;
132 + } else {
133 + $addons = $this->get_cached();
134 + }
135 +
89 136 if ( is_array( $addons ) ) {
90 137 return $addons;
91 138 }
92 139
@@ -98,8 +145,9 @@
98 145 $this->set_running();
99 146
100 147 // We need to know the version number to allow different downloads.
101 148 $agent = 'formidable/' . FrmAppHelper::plugin_version();
149 +
102 150 if ( class_exists( 'FrmProDb' ) ) {
103 151 $agent = 'formidable-pro/' . FrmProDb::$plug_version;
104 152 }
105 153
@@ -127,9 +175,10 @@
127 175 }
128 176
129 177 if ( isset( $addon['categories'] ) ) {
130 178 $cats = array_intersect( $this->skip_categories(), $addon['categories'] );
131 - if ( ! empty( $cats ) ) {
179 +
180 + if ( $cats ) {
132 181 unset( $addons[ $k ] );
133 182 continue;
134 183 }
135 184 }
@@ -165,8 +214,9 @@
165 214 * @return void
166 215 */
167 216 protected function set_running() {
168 217 $expires = 2 * MINUTE_IN_SECONDS;
218 +
169 219 if ( $this->run_as_multisite() ) {
170 220 set_site_transient( $this->transient_key(), true, $expires );
171 221 return;
172 222 }
@@ -194,9 +244,9 @@
194 244 *
195 245 * @return bool
196 246 */
197 247 protected function run_as_multisite() {
198 - return is_multisite() && empty( $this->license );
248 + return is_multisite() && ! $this->license;
199 249 }
200 250
201 251 /**
202 252 * @since 6.8.3
@@ -203,9 +253,9 @@
203 253 *
204 254 * @return string
205 255 */
206 256 protected function transient_key() {
207 - return strtolower( self::class ) . '_request_lock';
257 + return strtolower( static::class ) . '_request_lock';
208 258 }
209 259
210 260 /**
211 261 * @since 3.06
@@ -212,9 +262,9 @@
212 262 *
213 263 * @return string
214 264 */
215 265 protected function api_url() {
216 - if ( empty( $this->license ) ) {
266 + if ( ! $this->license ) {
217 267 // Direct traffic to Cloudflare worker when there is no license.
218 268 return 'https://plapi.formidableforms.com/list/';
219 269 }
220 270 return 'https://formidableforms.com/wp-json/s11edd/v1/updates/';
@@ -237,16 +287,18 @@
237 287 *
238 288 * @return array
239 289 */
240 290 public function get_addon_for_license( $license_plugin, $addons = array() ) {
241 - if ( empty( $addons ) ) {
291 + if ( ! $addons ) {
242 292 $addons = $this->get_api_info();
243 293 }
294 +
244 295 $download_id = $license_plugin->download_id;
245 296 $plugin = array();
246 - if ( empty( $download_id ) && ! empty( $addons ) ) {
297 +
298 + if ( ! $download_id && $addons ) {
247 299 foreach ( $addons as $addon ) {
248 - if ( is_array( $addon ) && ! empty( $addon['title'] ) && strtolower( $license_plugin->plugin_name ) === strtolower( $addon['title'] ) ) {
300 + if ( is_array( $addon ) && ! empty( $addon['title'] ) && 0 === strcasecmp( $license_plugin->plugin_name, $addon['title'] ) ) {
249 301 return $addon;
250 302 }
251 303 }
252 304 } elseif ( isset( $addons[ $download_id ] ) ) {
@@ -257,8 +309,10 @@
257 309 }
258 310
259 311 /**
260 312 * @since 3.06
313 + *
314 + * @return false|object
261 315 */
262 316 public function get_pro_updater() {
263 317 if ( FrmAppHelper::pro_is_installed() && is_callable( 'FrmProAppHelper::get_updater' ) ) {
264 318 $updater = FrmProAppHelper::get_updater();
@@ -276,33 +330,62 @@
276 330 * @return array|bool
277 331 */
278 332 protected function get_cached() {
279 333 $cache = $this->get_cached_option();
280 - if ( empty( $cache ) ) {
334 +
335 + if ( ! $cache ) {
281 336 return false;
282 337 }
283 338
284 - // If the api call is running, we can use the expired cache.
285 - if ( ! $this->is_running() ) {
286 - if ( empty( $cache['timeout'] ) || time() > $cache['timeout'] ) {
287 - // Cache is expired.
288 - return false;
289 - }
339 + $is_expired = empty( $cache['timeout'] ) || time() > $cache['timeout'];
290 340
291 - $version = FrmAppHelper::plugin_version();
292 - $for_current = isset( $cache['version'] ) && $cache['version'] == $version;
293 - if ( ! $for_current ) {
294 - // Force a new check.
295 - return false;
296 - }
341 + if ( ! $is_expired && isset( $cache['version'] ) && $cache['version'] !== FrmAppHelper::plugin_version() ) {
342 + $is_expired = true;
297 343 }
298 344
299 - $values = json_decode( $cache['value'], true );
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 + }
300 351
301 - return $values;
352 + return $this->decode_cached( $cache['value'] );
302 353 }
303 354
304 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 + /**
305 388 * Get the cache for the network if multisite.
306 389 *
307 390 * @since 6.8.3
308 391 *
@@ -310,8 +393,9 @@
310 393 */
311 394 protected function get_cached_option() {
312 395 if ( is_multisite() ) {
313 396 $cached = get_site_option( $this->cache_key );
397 +
314 398 if ( $cached ) {
315 399 return $cached;
316 400 }
317 401 }
@@ -326,8 +410,10 @@
326 410 *
327 411 * @return void
328 412 */
329 413 protected function set_cached( $addons ) {
414 + $addons = $this->reduce_addon_data_before_caching( $addons );
415 +
330 416 $data = array(
331 417 'timeout' => strtotime( $this->get_cache_timeout( $addons ), time() ),
332 418 'value' => wp_json_encode( $addons ),
333 419 'version' => FrmAppHelper::plugin_version(),
@@ -335,13 +421,83 @@
335 421
336 422 if ( is_multisite() ) {
337 423 update_site_option( $this->cache_key, $data );
338 424 } else {
339 - update_option( $this->cache_key, $data, 'no' );
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 );
340 428 }
341 429 }
342 430
343 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 + /**
344 500 * If the last check was a a rate limit, we'll need to check again sooner.
345 501 *
346 502 * @since 6.8.3
347 503 *
@@ -349,13 +505,12 @@
349 505 *
350 506 * @return string
351 507 */
352 508 protected function get_cache_timeout( $addons ) {
353 - $timeout = $this->cache_timeout;
354 509 if ( isset( $addons['response_code'] ) && 429 === $addons['response_code'] ) {
355 - $timeout = '+5 minutes';
510 + return '+5 minutes';
356 511 }
357 - return $timeout;
512 + return $this->cache_timeout;
358 513 }
359 514
360 515 /**
361 516 * @since 3.06
@@ -372,38 +527,41 @@
372 527 }
373 528
374 529 /**
375 530 * @since 3.06
531 + *
376 532 * @return array
377 533 */
378 534 public function error_for_license() {
379 - $errors = array();
380 - if ( ! empty( $this->license ) ) {
381 - $errors = $this->get_error_from_response();
382 - }
383 -
384 - return $errors;
535 + return $this->license ? $this->get_error_from_response() : array();
385 536 }
386 537
387 538 /**
388 539 * @since 3.06
540 + *
541 + * @param array $addons
542 + *
389 543 * @return array
390 544 */
391 545 public function get_error_from_response( $addons = array() ) {
392 - if ( empty( $addons ) ) {
546 + if ( ! $addons ) {
393 547 $addons = $this->get_api_info();
394 548 }
549 +
395 550 $errors = array();
396 - if ( isset( $addons['error'] ) ) {
397 - if ( is_string( $addons['error'] ) ) {
398 - $errors[] = $addons['error'];
399 - } elseif ( ! empty( $addons['error']['message'] ) ) {
400 - $errors[] = $addons['error']['message'];
401 - }
402 551
403 - do_action( 'frm_license_error', $addons['error'] );
552 + if ( ! isset( $addons['error'] ) ) {
553 + return $errors;
404 554 }
405 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 +
406 564 return $errors;
407 565 }
408 566
409 567 /**
@@ -411,8 +569,9 @@
411 569 *
412 570 * @since 6.0
413 571 *
414 572 * @param array $addon
573 + *
415 574 * @return bool
416 575 */
417 576 protected function is_new( $addon ) {
418 577 return strtotime( $addon['released'] ) > strtotime( '-' . $this->new_days . ' days' );