PluginProbe
Cooked – Recipe Management / 1.16.1
Cooked – Recipe Management v1.16.1
1.16.1 1.16.0 1.15.0 trunk 1.10.0 1.11.0 1.11.1 1.11.2 1.11.3 1.11.4 1.12.0 1.13.0 1.14.0 1.7.10 1.7.11 1.7.12 1.7.13 1.7.15.1 1.7.15.3 1.7.15.4 1.8.0 1.8.1 1.8.2 1.8.3 1.8.4 All 37 releases
cooked / includes / class.cooked-updates.php

class.cooked-updates.php in Cooked – Recipe Management 1.16.1, at includes/class.cooked-updates.php

547 lines 17.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Plugin Updates and Data Migrations
4 *
5 * @package Cooked_Updates
6 * @subpackage Cooked_Updates / Core
7 * @since 1.11.2
8 */
9
10 // Exit if accessed directly
11 if ( !defined( 'ABSPATH' ) ) exit;
12
13 /**
14 * Cooked_Updates Class
15 *
16 * This class handles all version updates and data migrations for the Cooked plugin.
17 * It follows WordPress best practices for plugin updates and ensures data integrity.
18 *
19 * @since 1.11.2
20 */
21 class Cooked_Updates {
22
23 /**
24 * Current plugin version
25 */
26 private static $current_version;
27
28 /**
29 * Previous plugin version
30 */
31 private static $previous_version;
32
33 /**
34 * Current pro plugin version
35 */
36 private static $current_pro_version;
37
38 /**
39 * Previous pro plugin version
40 */
41 private static $previous_pro_version;
42
43 /**
44 * Cooked Settings Saved
45 */
46 private static $cooked_settings_saved;
47
48 /**
49 * Whether rewrite rules were already flushed this request.
50 *
51 * @var bool
52 */
53 private static $rewrite_rules_flushed = false;
54
55 /**
56 * Initialize the updates system
57 */
58 public function __construct() {
59 // Add action to check version and update settings at the end of page load.
60 add_action( 'shutdown', [__CLASS__, 'init'] );
61 add_action( 'init', [ __CLASS__, 'maybe_heal_rewrite_rules' ], 99 );
62 }
63
64 /**
65 * Initialize the updates
66 */
67 public static function init() {
68 self::$cooked_settings_saved = get_option( 'cooked_settings_saved', false );
69 self::$current_version = COOKED_VERSION;
70 self::$previous_version = get_option( 'cooked_settings_version', '1.0.0' );
71 self::$current_pro_version = defined('COOKED_PRO_VERSION') ? COOKED_PRO_VERSION : null;
72 self::$previous_pro_version = get_option( 'cooked_pro_settings_version', '1.0.0' );
73
74 if ( !self::$cooked_settings_saved ) {
75 global $_cooked_settings;
76
77 if ( empty($_cooked_settings) ) {
78 $_cooked_settings = Cooked_Settings::get();
79 }
80
81 update_option( 'cooked_settings', $_cooked_settings );
82 self::$cooked_settings_saved = true;
83 }
84
85 // Only run updates if version has changed
86 if ( self::needs_update() ) {
87 self::run_updates();
88 }
89
90 self::maybe_flush_queued_rewrite_rules();
91 }
92
93 /**
94 * Check if an update is needed
95 *
96 * @return bool True if update is needed
97 */
98 public static function needs_update() {
99 // Check both versions.
100 $cooked_version_compare = version_compare( self::$previous_version, self::$current_version );
101 $cooked_pro_version_compare = ( defined('COOKED_PRO_VERSION') && self::$current_pro_version !== null ) ? version_compare( self::$previous_pro_version, self::$current_pro_version ) : 0;
102
103 // Update if either version has changed.
104 if ( $cooked_version_compare < 0 || $cooked_pro_version_compare < 0 ) {
105 return true;
106 }
107
108 return false;
109 }
110
111 /**
112 * Run all necessary updates
113 */
114 private static function run_updates() {
115 // Store the previous version for logging
116 $old_version = self::$previous_version;
117
118 // Run version-specific updates
119 self::run_version_updates();
120
121 self::update_rewrite_rules();
122
123 // Update both version numbers.
124 update_option( 'cooked_settings_version', self::$current_version );
125 if ( defined('COOKED_PRO_VERSION') ) {
126 update_option( 'cooked_pro_settings_version', self::$current_pro_version );
127 }
128
129 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
130 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
131 error_log( sprintf( 'Cooked: Updated from version %s to %s', $old_version, self::$current_version ) );
132 }
133 }
134
135 /**
136 * Run version-specific updates
137 */
138 private static function run_version_updates() {
139 $updates = self::get_version_updates();
140
141 foreach ( $updates as $version => $update_methods ) {
142 if ( version_compare( self::$previous_version, $version, '<' ) ) {
143 foreach ( $update_methods as $method ) {
144 if ( method_exists( __CLASS__, $method ) ) {
145 try {
146 call_user_func( [__CLASS__, $method] );
147 } catch ( Exception $e ) {
148 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
149 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
150 error_log( sprintf( 'Cooked: Error running update method %s: %s', $method, $e->getMessage() ) );
151 }
152 }
153 }
154 }
155 }
156 }
157 }
158
159 /**
160 * Get list of tools that can be run on demand (for Tools page and WP-CLI).
161 *
162 * @return array[] List of tools: [ 'id' => method_name, 'name' => label, 'description' => desc ]
163 */
164 public static function get_runnable_tools() {
165 $labels = [
166 'update_rewrite_rules' => [
167 'name' => __( 'Flush rewrite rules', 'cooked' ),
168 'description' => __( 'Refreshes permalink rules for recipe and profile URLs.', 'cooked' ),
169 ],
170 'fix_recipe_line_endings' => [
171 'name' => __( 'Fix recipe line endings', 'cooked' ),
172 'description' => __( 'Normalizes line endings in recipe content for exporter/importer compatibility.', 'cooked' ),
173 ],
174 'purge_legacy_related_recipes_cache' => [
175 'name' => __( 'Purge legacy related recipes cache', 'cooked' ),
176 'description' => __( 'Removes old transients and options from the previous related-recipes cache.', 'cooked' ),
177 ],
178 'remove_recipes_from_cooked_user_meta' => [
179 'name' => __( 'Remove recipes from user meta', 'cooked' ),
180 'description' => __( 'Removes the legacy "recipes" key from all users\' cooked_user_meta.', 'cooked' ),
181 ],
182 ];
183
184 $updates = self::get_version_updates();
185 $method_names = [];
186 foreach ( $updates as $methods ) {
187 foreach ( $methods as $method ) {
188 $method_names[ $method ] = true;
189 }
190 }
191 $method_names = array_keys( $method_names );
192
193 $tools = [];
194 foreach ( $method_names as $method ) {
195 $tools[] = [
196 'id' => $method,
197 'name' => isset( $labels[ $method ]['name'] ) ? $labels[ $method ]['name'] : $method,
198 'description' => isset( $labels[ $method ]['description'] ) ? $labels[ $method ]['description'] : '',
199 ];
200 }
201 return $tools;
202 }
203
204 /**
205 * Run a single migration/tool by name (on demand). Used by Tools page and WP-CLI.
206 *
207 * @param string $tool_name Method name (must exist in get_version_updates).
208 * @return true|WP_Error True on success, WP_Error on failure.
209 */
210 public static function run_tool( $tool_name ) {
211 $tool_name = is_string( $tool_name ) ? trim( $tool_name ) : '';
212 if ( $tool_name === '' ) {
213 return new \WP_Error( 'cooked_tool_empty', __( 'Tool name is required.', 'cooked' ) );
214 }
215
216 $updates = self::get_version_updates();
217 $allowed = [];
218 foreach ( $updates as $methods ) {
219 foreach ( $methods as $method ) {
220 $allowed[ $method ] = true;
221 }
222 }
223 if ( ! isset( $allowed[ $tool_name ] ) ) {
224 /* translators: %s: site health tool name */
225 return new \WP_Error( 'cooked_tool_invalid', sprintf( __( 'Unknown tool: %s.', 'cooked' ), $tool_name ) );
226 }
227 if ( ! method_exists( __CLASS__, $tool_name ) ) {
228 /* translators: %s: PHP method name */
229 return new \WP_Error( 'cooked_tool_missing', sprintf( __( 'Tool method %s does not exist.', 'cooked' ), $tool_name ) );
230 }
231
232 try {
233 call_user_func( [ __CLASS__, $tool_name ] );
234 return true;
235 } catch ( \Exception $e ) {
236 return new \WP_Error( 'cooked_tool_error', $e->getMessage() );
237 }
238 }
239
240 /**
241 * Define version-specific updates
242 *
243 * @return array Array of version updates with their corresponding methods
244 */
245 private static function get_version_updates() {
246 return apply_filters( 'cooked_version_updates', [
247 '1.9.0' => [
248 'update_rewrite_rules'
249 ],
250 '1.9.1' => [
251 'update_rewrite_rules'
252 ],
253 '1.9.2' => [
254 'update_rewrite_rules'
255 ],
256 '1.9.4' => [
257 'update_rewrite_rules'
258 ],
259 '1.9.5' => [
260 'update_rewrite_rules'
261 ],
262 '1.11.2' => [
263 'fix_recipe_line_endings',
264 'update_rewrite_rules'
265 ],
266 '1.13.0' => [
267 'purge_legacy_related_recipes_cache',
268 'remove_recipes_from_cooked_user_meta',
269 ],
270 ]);
271 }
272
273 /**
274 * Fix line endings in existing recipes to prevent WordPress exporter/importer issues
275 *
276 * @since 1.11.2
277 */
278 private static function fix_recipe_line_endings() {
279 // Get all recipe posts
280 $recipes = get_posts([
281 'post_type' => 'cp_recipe',
282 'posts_per_page' => -1,
283 'post_status' => 'any'
284 ]);
285
286 if ( empty($recipes) ) {
287 return;
288 }
289
290 $updated_count = 0;
291
292 foreach ( $recipes as $recipe ) {
293 $recipe_settings = get_post_meta( $recipe->ID, '_recipe_settings', true );
294
295 if ( empty($recipe_settings) ) {
296 continue;
297 }
298
299 $needs_update = false;
300
301 // Fix content field
302 if ( isset( $recipe_settings['content'] ) ) {
303 $original_content = $recipe_settings['content'];
304 $recipe_settings['content'] = str_replace( ["\r\n", "\r"], "\n", $recipe_settings['content'] );
305 if ( $original_content !== $recipe_settings['content'] ) {
306 $needs_update = true;
307 }
308 }
309
310 // Fix excerpt field
311 if ( isset( $recipe_settings['excerpt'] ) ) {
312 $original_excerpt = $recipe_settings['excerpt'];
313 $recipe_settings['excerpt'] = str_replace( ["\r\n", "\r"], "\n", $recipe_settings['excerpt'] );
314 if ( $original_excerpt !== $recipe_settings['excerpt'] ) {
315 $needs_update = true;
316 }
317 }
318
319 // Fix notes field
320 if ( isset( $recipe_settings['notes'] ) ) {
321 $original_notes = $recipe_settings['notes'];
322 $recipe_settings['notes'] = str_replace( ["\r\n", "\r"], "\n", $recipe_settings['notes'] );
323 if ( $original_notes !== $recipe_settings['notes'] ) {
324 $needs_update = true;
325 }
326 }
327
328 // Fix directions content
329 if ( isset( $recipe_settings['directions'] ) && is_array( $recipe_settings['directions'] ) ) {
330 foreach ( $recipe_settings['directions'] as $key => $direction ) {
331 if ( isset( $direction['content'] ) ) {
332 $original_direction_content = $direction['content'];
333 $recipe_settings['directions'][$key]['content'] = str_replace( ["\r\n", "\r"], "\n", $direction['content'] );
334 if ( $original_direction_content !== $recipe_settings['directions'][$key]['content'] ) {
335 $needs_update = true;
336 }
337 }
338 }
339 }
340
341 // Update the recipe if changes were made
342 if ( $needs_update ) {
343 update_post_meta( $recipe->ID, '_recipe_settings', $recipe_settings );
344 $updated_count++;
345 }
346 }
347
348 // Log the update if any recipes were modified
349 if ( $updated_count > 0 && defined( 'WP_DEBUG' ) && WP_DEBUG ) {
350 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
351 error_log( sprintf( 'Cooked: Fixed line endings in %d recipes for WordPress exporter/importer compatibility.', $updated_count ) );
352 }
353 }
354
355 /**
356 * Purge legacy related-recipes transients and options (cache and pre-calculation data).
357 * One-time cleanup when moving to on-demand related recipes with no cache.
358 *
359 * @since 1.13.0
360 */
361 private static function purge_legacy_related_recipes_cache() {
362 global $wpdb;
363
364 $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_cooked_related_%' OR option_name LIKE '_transient_timeout_cooked_related_%'" );
365 delete_option( 'cooked_related_version' );
366 delete_option( 'cooked_related_calculation_last' );
367
368 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
369 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
370 error_log( 'Cooked: Purged legacy related-recipes cache and options.' );
371 }
372 }
373
374 /**
375 * Remove legacy 'recipes' key from cooked_user_meta for all users.
376 * Recipes are computed on demand in Cooked_Users::get(); they must not be stored in user meta.
377 *
378 * @since 1.13.0
379 */
380 private static function remove_recipes_from_cooked_user_meta() {
381 $user_ids = get_users( [
382 'meta_key' => 'cooked_user_meta',
383 'fields' => 'ID',
384 ] );
385
386 if ( empty( $user_ids ) ) {
387 return;
388 }
389
390 $updated_count = 0;
391
392 foreach ( $user_ids as $user_id ) {
393 $meta = get_user_meta( $user_id, 'cooked_user_meta', true );
394
395 if ( ! is_array( $meta ) || ! isset( $meta['recipes'] ) ) {
396 continue;
397 }
398
399 unset( $meta['recipes'] );
400 update_user_meta( $user_id, 'cooked_user_meta', $meta );
401 $updated_count++;
402 }
403
404 if ( $updated_count > 0 && defined( 'WP_DEBUG' ) && WP_DEBUG ) {
405 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
406 error_log( sprintf( 'Cooked: Removed legacy recipes key from %d user(s) cooked_user_meta.', $updated_count ) );
407 }
408 }
409
410 /**
411 * Soft-flush rewrite rules when any rule registered this request is missing
412 * from the stored rewrite_rules option.
413 *
414 * @since 1.16.0
415 * @return void
416 */
417 public static function maybe_heal_rewrite_rules() {
418 if ( self::registered_rewrite_rules_missing() ) {
419 self::update_rewrite_rules();
420 }
421 }
422
423 /**
424 * Queue a one-shot rewrite flush when registered rewrite rules are missing
425 * from the stored rewrite_rules option.
426 *
427 * @since 1.16.0
428 * @return void
429 */
430 public static function maybe_queue_rewrite_flush() {
431 if ( self::registered_rewrite_rules_missing() ) {
432 update_option( 'cooked_flush_rewrite_rules', '1' );
433 }
434 }
435
436 /**
437 * Whether the stored rewrite_rules option is missing any rewrite rule
438 * registered this request via add_rewrite_rule().
439 *
440 * @since 1.16.0
441 * @return bool
442 */
443 public static function registered_rewrite_rules_missing() {
444 if ( function_exists( 'wp_installing' ) && wp_installing() ) {
445 return false;
446 }
447
448 if ( ! get_option( 'permalink_structure' ) ) {
449 return false;
450 }
451
452 $registered = self::registered_extra_rewrite_rules();
453 if ( empty( $registered ) ) {
454 return false;
455 }
456
457 $stored = get_option( 'rewrite_rules' );
458 if ( ! is_array( $stored ) || empty( $stored ) ) {
459 return true;
460 }
461
462 foreach ( $registered as $regex => $query ) {
463 if ( array_key_exists( $regex, $stored ) ) {
464 continue;
465 }
466
467 if ( is_string( $query ) && in_array( $query, $stored, true ) ) {
468 continue;
469 }
470
471 return true;
472 }
473
474 return false;
475 }
476
477 /**
478 * Rewrite rules added this request with add_rewrite_rule().
479 *
480 * @since 1.16.0
481 * @return array
482 */
483 private static function registered_extra_rewrite_rules() {
484 global $wp_rewrite;
485
486 if ( ! is_object( $wp_rewrite ) ) {
487 return [];
488 }
489
490 $rules = [];
491
492 if ( ! empty( $wp_rewrite->extra_rules_top ) && is_array( $wp_rewrite->extra_rules_top ) ) {
493 $rules = $wp_rewrite->extra_rules_top;
494 }
495
496 if ( ! empty( $wp_rewrite->extra_rules ) && is_array( $wp_rewrite->extra_rules ) ) {
497 $rules = array_merge( $rules, $wp_rewrite->extra_rules );
498 }
499
500 return $rules;
501 }
502
503 /**
504 * Soft-flush rewrite rules when a previous request queued it.
505 *
506 * @since 1.16.0
507 * @return void
508 */
509 public static function maybe_flush_queued_rewrite_rules() {
510 if ( ! get_option( 'cooked_flush_rewrite_rules' ) ) {
511 return;
512 }
513
514 self::update_rewrite_rules();
515 delete_option( 'cooked_flush_rewrite_rules' );
516 }
517
518 /**
519 * Reset per-request flush state. Used by tests.
520 *
521 * @since 1.16.0
522 * @return void
523 */
524 public static function reset_rewrite_flush_state() {
525 self::$rewrite_rules_flushed = false;
526 }
527
528 /**
529 * Update rewrite rules if needed
530 *
531 * @since 1.11.2
532 */
533 private static function update_rewrite_rules() {
534 if ( self::$rewrite_rules_flushed ) {
535 return;
536 }
537
538 flush_rewrite_rules( false );
539 self::$rewrite_rules_flushed = true;
540 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
541 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
542 error_log( 'Cooked: Flushed rewrite rules due to version update.' );
543 }
544 }
545
546 }
547