Changeset 3151025
- Timestamp:
- 09/12/2024 05:50:16 PM (7 months ago)
- Location:
- content-update-scheduler
- Files:
-
- 2 edited
- 3 copied
Legend:
- Unmodified
- Added
- Removed
-
content-update-scheduler/tags/2.3.2/content-update-scheduler.php
r3137019 r3151025 8 8 * Author: Infinitnet 9 9 * Author URI: https://infinitnet.io/ 10 * Version: 2.3. 110 * Version: 2.3.2 11 11 * License: GPLv3 12 12 * Text Domain: content-update-scheduler … … 937 937 error_log("Calculated timestamp: $stamp"); 938 938 939 if ($stamp <= time()) { 940 $stamp = time() + 300; // 5 minutes from now 939 $current_time = new DateTime('now', $tz); 940 if ($date_time <= $current_time) { 941 $date_time = clone $current_time; 942 $date_time->modify('+5 minutes'); 943 $stamp = $date_time->getTimestamp(); 941 944 error_log("Timestamp was in the past, set to 5 minutes from now: $stamp"); 942 945 } … … 990 993 * Publishes a scheduled update 991 994 * 992 * Copies the original post's contents and meta into it 's "scheduled update" and then deletes993 * the originalpost. This function is either called by wp_cron or if the user hits the995 * Copies the original post's contents and meta into its "scheduled update" and then deletes 996 * the scheduled post. This function is either called by wp_cron or if the user hits the 994 997 * 'publish now' action 995 998 * 996 999 * @param int $post_id the post's id. 997 1000 * 998 * @return int the original post's id1001 * @return int|WP_Error the original post's id or WP_Error on failure 999 1002 */ 1000 1003 public static function publish_post($post_id) 1001 1004 { 1002 1005 error_log("publish_post called for post ID: " . $post_id); 1003 $orig_id = get_post_meta($post_id, self::$_cus_publish_status . '_original', true); 1004 1005 // break early if given post is not an actual scheduled post created by this plugin. 1006 if (! $orig_id) { 1007 error_log("No original post found for post ID: " . $post_id); 1008 return $post_id; 1009 } 1010 1011 $orig = get_post($orig_id); 1012 if (!$orig) { 1013 error_log("Original post not found for ID: " . $orig_id); 1014 return $post_id; 1015 } 1016 1017 $post = get_post($post_id); 1018 if (!$post) { 1019 error_log("Scheduled post not found for ID: " . $post_id); 1020 return $post_id; 1021 } 1022 1023 // Ensure the post is not in the trash before proceeding 1024 if ($post->post_status === 'trash') { 1025 error_log("Post is in trash, aborting publish process for post ID: " . $post_id); 1026 return $post_id; 1027 } 1028 1029 $original_stock_status = get_post_meta($orig->ID, '_stock_status', true); 1030 $original_stock_quantity = get_post_meta($orig->ID, '_stock', true); 1031 1032 self::handle_plugin_css_copy($post->ID, $orig_id); 1033 1034 /** 1035 * Fires before a scheduled post is being updated 1036 * 1037 * @param WP_Post $post the scheduled update post. 1038 * @param WP_post $orig the original post. 1039 */ 1040 do_action('ContentUpdateScheduler\\before_publish_post', $post, $orig); 1041 delete_post_meta($orig->ID, self::$_cus_publish_status . '_pubdate'); 1042 // Copy meta and terms, restoring references to the original post ID 1043 self::copy_meta_and_terms($post->ID, $orig->ID, true); 1044 1045 $post->ID = $orig->ID; 1046 $post->post_name = $orig->post_name; 1047 $post->guid = $orig->guid; 1048 $post->post_parent = $orig->post_parent; 1049 $post->post_status = $orig->post_status; 1050 $post_date = wp_date('Y-m-d H:i:s'); 1051 1052 /** 1053 * Filter the new posts' post date 1054 * 1055 * @param string $post_date the date to be used, must be in the form of `Y-m-d H:i:s`. 1056 * @param WP_Post $post the scheduled update post. 1057 * @param WP_Post $orig the original post. 1058 */ 1059 $post_date = apply_filters('ContentUpdateScheduler\\publish_post_date', $post_date, $post, $orig); 1060 1061 $post->post_date = $post_date; // we need this to get wp to recognize this as a newly updated post. 1062 $post->post_date_gmt = get_gmt_from_date($post_date); 1063 1064 delete_post_meta($orig->ID, self::$_cus_publish_status . '_pubdate'); 1065 1066 $result = wp_update_post($post, true); 1067 if (is_wp_error($result)) { 1068 error_log("Error updating post: " . $result->get_error_message()); 1069 return $result; 1070 } 1071 1072 if ($original_stock_status !== '') { 1073 update_post_meta($post->ID, '_stock_status', $original_stock_status); 1074 } 1075 if ($original_stock_quantity !== '') { 1076 update_post_meta($post->ID, '_stock', $original_stock_quantity); 1077 } 1078 1079 $delete_result = wp_delete_post($post_id, true); 1080 if (is_wp_error($delete_result)) { 1081 error_log("Error deleting scheduled post: " . $delete_result->get_error_message()); 1082 return $delete_result; 1083 } 1084 1085 error_log("Successfully published post. Original ID: " . $orig->ID); 1086 return $orig->ID; 1006 1007 // Implement locking mechanism 1008 $lock_key = 'cus_publish_lock_' . $post_id; 1009 if (!get_transient($lock_key)) { 1010 set_transient($lock_key, true, 300); // Lock for 5 minutes 1011 } else { 1012 error_log("Publish process already running for post ID: " . $post_id); 1013 return new WP_Error('locked', 'Publish process already running for this post'); 1014 } 1015 1016 try { 1017 $orig_id = get_post_meta($post_id, self::$_cus_publish_status . '_original', true); 1018 1019 // break early if given post is not an actual scheduled post created by this plugin. 1020 if (!$orig_id) { 1021 error_log("No original post found for post ID: " . $post_id); 1022 return new WP_Error('no_original', 'No original post found'); 1023 } 1024 1025 $orig = get_post($orig_id); 1026 if (!$orig) { 1027 error_log("Original post not found for ID: " . $orig_id); 1028 return new WP_Error('original_not_found', 'Original post not found'); 1029 } 1030 1031 $post = get_post($post_id); 1032 if (!$post) { 1033 error_log("Scheduled post not found for ID: " . $post_id); 1034 return new WP_Error('scheduled_not_found', 'Scheduled post not found'); 1035 } 1036 1037 // Ensure the post is not in the trash before proceeding 1038 if ($post->post_status === 'trash') { 1039 error_log("Post is in trash, aborting publish process for post ID: " . $post_id); 1040 return new WP_Error('post_trashed', 'Post is in trash'); 1041 } 1042 1043 $original_stock_status = get_post_meta($orig->ID, '_stock_status', true); 1044 $original_stock_quantity = get_post_meta($orig->ID, '_stock', true); 1045 1046 self::handle_plugin_css_copy($post->ID, $orig_id); 1047 1048 /** 1049 * Fires before a scheduled post is being updated 1050 * 1051 * @param WP_Post $post the scheduled update post. 1052 * @param WP_post $orig the original post. 1053 */ 1054 do_action('ContentUpdateScheduler\\before_publish_post', $post, $orig); 1055 1056 // Start "transaction" 1057 global $wpdb; 1058 $wpdb->query('START TRANSACTION'); 1059 1060 delete_post_meta($orig->ID, self::$_cus_publish_status . '_pubdate'); 1061 // Copy meta and terms, restoring references to the original post ID 1062 self::copy_meta_and_terms($post->ID, $orig->ID, true); 1063 1064 $post->ID = $orig->ID; 1065 $post->post_name = $orig->post_name; 1066 $post->guid = $orig->guid; 1067 $post->post_parent = $orig->post_parent; 1068 $post->post_status = $orig->post_status; 1069 $post_date = wp_date('Y-m-d H:i:s'); 1070 1071 /** 1072 * Filter the new posts' post date 1073 * 1074 * @param string $post_date the date to be used, must be in the form of `Y-m-d H:i:s`. 1075 * @param WP_Post $post the scheduled update post. 1076 * @param WP_Post $orig the original post. 1077 */ 1078 $post_date = apply_filters('ContentUpdateScheduler\\publish_post_date', $post_date, $post, $orig); 1079 1080 $post->post_date = $post_date; 1081 $post->post_date_gmt = get_gmt_from_date($post_date); 1082 1083 delete_post_meta($orig->ID, self::$_cus_publish_status . '_pubdate'); 1084 1085 $result = wp_update_post($post, true); 1086 if (is_wp_error($result)) { 1087 error_log("Error updating post: " . $result->get_error_message()); 1088 $wpdb->query('ROLLBACK'); 1089 return $result; 1090 } 1091 1092 if ($original_stock_status !== '') { 1093 update_post_meta($post->ID, '_stock_status', $original_stock_status); 1094 } 1095 if ($original_stock_quantity !== '') { 1096 update_post_meta($post->ID, '_stock', $original_stock_quantity); 1097 } 1098 1099 $delete_result = wp_delete_post($post_id, true); 1100 if (is_wp_error($delete_result)) { 1101 error_log("Error deleting scheduled post: " . $delete_result->get_error_message()); 1102 $wpdb->query('ROLLBACK'); 1103 return $delete_result; 1104 } 1105 1106 $wpdb->query('COMMIT'); 1107 1108 error_log("Successfully published post. Original ID: " . $orig->ID); 1109 return $orig->ID; 1110 } catch (Exception $e) { 1111 error_log("Exception occurred during publish process: " . $e->getMessage()); 1112 $wpdb->query('ROLLBACK'); 1113 return new WP_Error('publish_exception', $e->getMessage()); 1114 } finally { 1115 delete_transient($lock_key); 1116 } 1087 1117 } 1088 1118 … … 1101 1131 $result = self::publish_post($ID); 1102 1132 kses_init_filters(); 1103 error_log("cron_publish_post completed for post ID: " . $ID . ". Result: " . print_r($result, true)); 1133 1134 if (is_wp_error($result)) { 1135 error_log("Error in cron_publish_post for post ID: " . $ID . ". Error: " . $result->get_error_message()); 1136 // Here you might want to add some error handling, such as notifying an admin or rescheduling the event 1137 } else { 1138 error_log("cron_publish_post completed successfully for post ID: " . $ID . ". Result: " . print_r($result, true)); 1139 } 1104 1140 } 1105 1141 … … 1235 1271 error_log("Checking for overdue posts to publish"); 1236 1272 global $wpdb; 1237 $current_time = current_time('timestamp'); 1238 1273 1274 // Get the WordPress timezone setting 1275 $wp_timezone = wp_timezone(); 1276 $current_time = new DateTime('now', $wp_timezone); 1277 $current_timestamp = $current_time->getTimestamp(); 1278 1279 error_log("Current time in WordPress timezone: " . $current_time->format('Y-m-d H:i:s')); 1280 1239 1281 $overdue_posts = $wpdb->get_results( 1240 1282 $wpdb->prepare( 1241 "SELECT post_id FROM {$wpdb->postmeta} 1242 WHERE meta_key = %s 1243 AND meta_value <= %d", 1244 self::$_cus_publish_status . '_pubdate', 1245 $current_time 1283 "SELECT post_id, meta_value FROM {$wpdb->postmeta} 1284 WHERE meta_key = %s", 1285 self::$_cus_publish_status . '_pubdate' 1246 1286 ) 1247 1287 ); 1248 1288 1249 1289 foreach ($overdue_posts as $post) { 1250 error_log("Publishing overdue post ID: " . $post->post_id); 1251 self::cron_publish_post($post->post_id); 1290 $scheduled_time = new DateTime('@' . $post->meta_value, $wp_timezone); 1291 error_log("Post ID: " . $post->post_id . " scheduled for: " . $scheduled_time->format('Y-m-d H:i:s')); 1292 1293 if ($scheduled_time <= $current_time) { 1294 error_log("Publishing overdue post ID: " . $post->post_id); 1295 self::cron_publish_post($post->post_id); 1296 } 1252 1297 } 1253 1298 } -
content-update-scheduler/tags/2.3.2/readme.txt
r3137019 r3151025 4 4 Requires at least: 5.0 5 5 Tested up to: 6.6.1 6 Stable tag: 2.3. 16 Stable tag: 2.3.2 7 7 Requires PHP: 7.3 8 8 License: GPLv3 … … 50 50 51 51 == Changelog == 52 53 = 2.3.2 = 54 * fix: Implement locking and transaction-like mechanism in publish_post 55 * fix: Use WordPress timezone for scheduling and publishing 52 56 53 57 = 2.3.1 = -
content-update-scheduler/trunk/content-update-scheduler.php
r3137019 r3151025 8 8 * Author: Infinitnet 9 9 * Author URI: https://infinitnet.io/ 10 * Version: 2.3. 110 * Version: 2.3.2 11 11 * License: GPLv3 12 12 * Text Domain: content-update-scheduler … … 937 937 error_log("Calculated timestamp: $stamp"); 938 938 939 if ($stamp <= time()) { 940 $stamp = time() + 300; // 5 minutes from now 939 $current_time = new DateTime('now', $tz); 940 if ($date_time <= $current_time) { 941 $date_time = clone $current_time; 942 $date_time->modify('+5 minutes'); 943 $stamp = $date_time->getTimestamp(); 941 944 error_log("Timestamp was in the past, set to 5 minutes from now: $stamp"); 942 945 } … … 990 993 * Publishes a scheduled update 991 994 * 992 * Copies the original post's contents and meta into it 's "scheduled update" and then deletes993 * the originalpost. This function is either called by wp_cron or if the user hits the995 * Copies the original post's contents and meta into its "scheduled update" and then deletes 996 * the scheduled post. This function is either called by wp_cron or if the user hits the 994 997 * 'publish now' action 995 998 * 996 999 * @param int $post_id the post's id. 997 1000 * 998 * @return int the original post's id1001 * @return int|WP_Error the original post's id or WP_Error on failure 999 1002 */ 1000 1003 public static function publish_post($post_id) 1001 1004 { 1002 1005 error_log("publish_post called for post ID: " . $post_id); 1003 $orig_id = get_post_meta($post_id, self::$_cus_publish_status . '_original', true); 1004 1005 // break early if given post is not an actual scheduled post created by this plugin. 1006 if (! $orig_id) { 1007 error_log("No original post found for post ID: " . $post_id); 1008 return $post_id; 1009 } 1010 1011 $orig = get_post($orig_id); 1012 if (!$orig) { 1013 error_log("Original post not found for ID: " . $orig_id); 1014 return $post_id; 1015 } 1016 1017 $post = get_post($post_id); 1018 if (!$post) { 1019 error_log("Scheduled post not found for ID: " . $post_id); 1020 return $post_id; 1021 } 1022 1023 // Ensure the post is not in the trash before proceeding 1024 if ($post->post_status === 'trash') { 1025 error_log("Post is in trash, aborting publish process for post ID: " . $post_id); 1026 return $post_id; 1027 } 1028 1029 $original_stock_status = get_post_meta($orig->ID, '_stock_status', true); 1030 $original_stock_quantity = get_post_meta($orig->ID, '_stock', true); 1031 1032 self::handle_plugin_css_copy($post->ID, $orig_id); 1033 1034 /** 1035 * Fires before a scheduled post is being updated 1036 * 1037 * @param WP_Post $post the scheduled update post. 1038 * @param WP_post $orig the original post. 1039 */ 1040 do_action('ContentUpdateScheduler\\before_publish_post', $post, $orig); 1041 delete_post_meta($orig->ID, self::$_cus_publish_status . '_pubdate'); 1042 // Copy meta and terms, restoring references to the original post ID 1043 self::copy_meta_and_terms($post->ID, $orig->ID, true); 1044 1045 $post->ID = $orig->ID; 1046 $post->post_name = $orig->post_name; 1047 $post->guid = $orig->guid; 1048 $post->post_parent = $orig->post_parent; 1049 $post->post_status = $orig->post_status; 1050 $post_date = wp_date('Y-m-d H:i:s'); 1051 1052 /** 1053 * Filter the new posts' post date 1054 * 1055 * @param string $post_date the date to be used, must be in the form of `Y-m-d H:i:s`. 1056 * @param WP_Post $post the scheduled update post. 1057 * @param WP_Post $orig the original post. 1058 */ 1059 $post_date = apply_filters('ContentUpdateScheduler\\publish_post_date', $post_date, $post, $orig); 1060 1061 $post->post_date = $post_date; // we need this to get wp to recognize this as a newly updated post. 1062 $post->post_date_gmt = get_gmt_from_date($post_date); 1063 1064 delete_post_meta($orig->ID, self::$_cus_publish_status . '_pubdate'); 1065 1066 $result = wp_update_post($post, true); 1067 if (is_wp_error($result)) { 1068 error_log("Error updating post: " . $result->get_error_message()); 1069 return $result; 1070 } 1071 1072 if ($original_stock_status !== '') { 1073 update_post_meta($post->ID, '_stock_status', $original_stock_status); 1074 } 1075 if ($original_stock_quantity !== '') { 1076 update_post_meta($post->ID, '_stock', $original_stock_quantity); 1077 } 1078 1079 $delete_result = wp_delete_post($post_id, true); 1080 if (is_wp_error($delete_result)) { 1081 error_log("Error deleting scheduled post: " . $delete_result->get_error_message()); 1082 return $delete_result; 1083 } 1084 1085 error_log("Successfully published post. Original ID: " . $orig->ID); 1086 return $orig->ID; 1006 1007 // Implement locking mechanism 1008 $lock_key = 'cus_publish_lock_' . $post_id; 1009 if (!get_transient($lock_key)) { 1010 set_transient($lock_key, true, 300); // Lock for 5 minutes 1011 } else { 1012 error_log("Publish process already running for post ID: " . $post_id); 1013 return new WP_Error('locked', 'Publish process already running for this post'); 1014 } 1015 1016 try { 1017 $orig_id = get_post_meta($post_id, self::$_cus_publish_status . '_original', true); 1018 1019 // break early if given post is not an actual scheduled post created by this plugin. 1020 if (!$orig_id) { 1021 error_log("No original post found for post ID: " . $post_id); 1022 return new WP_Error('no_original', 'No original post found'); 1023 } 1024 1025 $orig = get_post($orig_id); 1026 if (!$orig) { 1027 error_log("Original post not found for ID: " . $orig_id); 1028 return new WP_Error('original_not_found', 'Original post not found'); 1029 } 1030 1031 $post = get_post($post_id); 1032 if (!$post) { 1033 error_log("Scheduled post not found for ID: " . $post_id); 1034 return new WP_Error('scheduled_not_found', 'Scheduled post not found'); 1035 } 1036 1037 // Ensure the post is not in the trash before proceeding 1038 if ($post->post_status === 'trash') { 1039 error_log("Post is in trash, aborting publish process for post ID: " . $post_id); 1040 return new WP_Error('post_trashed', 'Post is in trash'); 1041 } 1042 1043 $original_stock_status = get_post_meta($orig->ID, '_stock_status', true); 1044 $original_stock_quantity = get_post_meta($orig->ID, '_stock', true); 1045 1046 self::handle_plugin_css_copy($post->ID, $orig_id); 1047 1048 /** 1049 * Fires before a scheduled post is being updated 1050 * 1051 * @param WP_Post $post the scheduled update post. 1052 * @param WP_post $orig the original post. 1053 */ 1054 do_action('ContentUpdateScheduler\\before_publish_post', $post, $orig); 1055 1056 // Start "transaction" 1057 global $wpdb; 1058 $wpdb->query('START TRANSACTION'); 1059 1060 delete_post_meta($orig->ID, self::$_cus_publish_status . '_pubdate'); 1061 // Copy meta and terms, restoring references to the original post ID 1062 self::copy_meta_and_terms($post->ID, $orig->ID, true); 1063 1064 $post->ID = $orig->ID; 1065 $post->post_name = $orig->post_name; 1066 $post->guid = $orig->guid; 1067 $post->post_parent = $orig->post_parent; 1068 $post->post_status = $orig->post_status; 1069 $post_date = wp_date('Y-m-d H:i:s'); 1070 1071 /** 1072 * Filter the new posts' post date 1073 * 1074 * @param string $post_date the date to be used, must be in the form of `Y-m-d H:i:s`. 1075 * @param WP_Post $post the scheduled update post. 1076 * @param WP_Post $orig the original post. 1077 */ 1078 $post_date = apply_filters('ContentUpdateScheduler\\publish_post_date', $post_date, $post, $orig); 1079 1080 $post->post_date = $post_date; 1081 $post->post_date_gmt = get_gmt_from_date($post_date); 1082 1083 delete_post_meta($orig->ID, self::$_cus_publish_status . '_pubdate'); 1084 1085 $result = wp_update_post($post, true); 1086 if (is_wp_error($result)) { 1087 error_log("Error updating post: " . $result->get_error_message()); 1088 $wpdb->query('ROLLBACK'); 1089 return $result; 1090 } 1091 1092 if ($original_stock_status !== '') { 1093 update_post_meta($post->ID, '_stock_status', $original_stock_status); 1094 } 1095 if ($original_stock_quantity !== '') { 1096 update_post_meta($post->ID, '_stock', $original_stock_quantity); 1097 } 1098 1099 $delete_result = wp_delete_post($post_id, true); 1100 if (is_wp_error($delete_result)) { 1101 error_log("Error deleting scheduled post: " . $delete_result->get_error_message()); 1102 $wpdb->query('ROLLBACK'); 1103 return $delete_result; 1104 } 1105 1106 $wpdb->query('COMMIT'); 1107 1108 error_log("Successfully published post. Original ID: " . $orig->ID); 1109 return $orig->ID; 1110 } catch (Exception $e) { 1111 error_log("Exception occurred during publish process: " . $e->getMessage()); 1112 $wpdb->query('ROLLBACK'); 1113 return new WP_Error('publish_exception', $e->getMessage()); 1114 } finally { 1115 delete_transient($lock_key); 1116 } 1087 1117 } 1088 1118 … … 1101 1131 $result = self::publish_post($ID); 1102 1132 kses_init_filters(); 1103 error_log("cron_publish_post completed for post ID: " . $ID . ". Result: " . print_r($result, true)); 1133 1134 if (is_wp_error($result)) { 1135 error_log("Error in cron_publish_post for post ID: " . $ID . ". Error: " . $result->get_error_message()); 1136 // Here you might want to add some error handling, such as notifying an admin or rescheduling the event 1137 } else { 1138 error_log("cron_publish_post completed successfully for post ID: " . $ID . ". Result: " . print_r($result, true)); 1139 } 1104 1140 } 1105 1141 … … 1235 1271 error_log("Checking for overdue posts to publish"); 1236 1272 global $wpdb; 1237 $current_time = current_time('timestamp'); 1238 1273 1274 // Get the WordPress timezone setting 1275 $wp_timezone = wp_timezone(); 1276 $current_time = new DateTime('now', $wp_timezone); 1277 $current_timestamp = $current_time->getTimestamp(); 1278 1279 error_log("Current time in WordPress timezone: " . $current_time->format('Y-m-d H:i:s')); 1280 1239 1281 $overdue_posts = $wpdb->get_results( 1240 1282 $wpdb->prepare( 1241 "SELECT post_id FROM {$wpdb->postmeta} 1242 WHERE meta_key = %s 1243 AND meta_value <= %d", 1244 self::$_cus_publish_status . '_pubdate', 1245 $current_time 1283 "SELECT post_id, meta_value FROM {$wpdb->postmeta} 1284 WHERE meta_key = %s", 1285 self::$_cus_publish_status . '_pubdate' 1246 1286 ) 1247 1287 ); 1248 1288 1249 1289 foreach ($overdue_posts as $post) { 1250 error_log("Publishing overdue post ID: " . $post->post_id); 1251 self::cron_publish_post($post->post_id); 1290 $scheduled_time = new DateTime('@' . $post->meta_value, $wp_timezone); 1291 error_log("Post ID: " . $post->post_id . " scheduled for: " . $scheduled_time->format('Y-m-d H:i:s')); 1292 1293 if ($scheduled_time <= $current_time) { 1294 error_log("Publishing overdue post ID: " . $post->post_id); 1295 self::cron_publish_post($post->post_id); 1296 } 1252 1297 } 1253 1298 } -
content-update-scheduler/trunk/readme.txt
r3137019 r3151025 4 4 Requires at least: 5.0 5 5 Tested up to: 6.6.1 6 Stable tag: 2.3. 16 Stable tag: 2.3.2 7 7 Requires PHP: 7.3 8 8 License: GPLv3 … … 50 50 51 51 == Changelog == 52 53 = 2.3.2 = 54 * fix: Implement locking and transaction-like mechanism in publish_post 55 * fix: Use WordPress timezone for scheduling and publishing 52 56 53 57 = 2.3.1 =
Note: See TracChangeset
for help on using the changeset viewer.