Changeset 3447091
- Timestamp:
- 01/26/2026 12:50:47 PM (2 months ago)
- Location:
- ringier-bus/trunk
- Files:
-
- 1 added
- 11 edited
-
CHANGELOG.md (modified) (1 diff)
-
README.md (modified) (1 diff)
-
assets/js/sync-tools.js (modified) (14 diffs)
-
readme.txt (modified) (2 diffs)
-
ringier-bus.php (modified) (2 diffs)
-
src/Core/AdminSyncPage.php (modified) (3 diffs)
-
src/Core/Bus/ArticlesEvent.php (added)
-
src/Core/Bus/BusHelper.php (modified) (2 diffs)
-
src/Core/BusPluginClass.php (modified) (1 diff)
-
src/Core/Utils.php (modified) (1 diff)
-
views/admin/admin-sync-page.php (modified) (4 diffs)
-
views/admin/button-flush-transient.php (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
ringier-bus/trunk/CHANGELOG.md
r3412229 r3447091 13 13 * (dependency) Fully remove Guzzle/Symfony dependencies 14 14 * (dependency) Fully remove Timber/Twig 15 16 17 ## [3.5.0] - 2026-01-21 ## 18 19 ### Added ### 20 * (UI) **Batch Article Sync** tool added to the "BUS Tooling Page". 21 * Features a "Recent First" sync strategy to prioritize the newest content. 22 * Includes a Post Type selector (Radio button) to allow syncing specific custom post types. 23 * Displays real-time progress logs in the admin dashboard. 24 * (code) Implemented a **Reverse ID Cursor** strategy for the Article Sync logic. 25 * Ensures **O(1)** constant performance regardless of database size (efficiently handles 10k+ posts). 26 * Replaces standard `OFFSET` pagination to prevent timeout issues on deep database queries. 27 28 ### Changed ### 29 * (code) Major refactor of the `ArticleEvent` class to remove 3rd-party dependencies in favor of 100% native WP code: 30 * Removed `GuzzleHttp\Client` in favor of native `wp_remote_post()`. 31 * Removed `AuthenticationInterface` dependency; now uses `BusTokenManager` directly. 15 32 16 33 -
ringier-bus/trunk/README.md
r3412229 r3447091 7 7 **Requires at least:** 6.0 8 8 **Tested up to:** 6.9 9 **Stable tag:** 3. 4.19 **Stable tag:** 3.5.0 10 10 **Requires PHP:** 8.1 11 11 **License:** GPLv2 or later -
ringier-bus/trunk/assets/js/sync-tools.js
r3412229 r3447091 3 3 const progressDivCat = $('#sync-progress-cat'); 4 4 const progressDivTag = $('#sync-progress-tag'); 5 const progressDivArticle = $('#sync-progress-article'); 5 6 6 7 function syncAuthors() { … … 9 10 let skippedCount = 0; 10 11 12 progressDiv.show(); 11 13 progressDiv.html(` 12 14 <h3>Starting to sync all authors having the following roles:</h3> … … 24 26 const { message, done, skipped } = response.data; 25 27 26 // 1. Check if we are done first 27 if (done) { 28 // If done, we stop the loop and print totals immediately. 29 // We do NOT increment successCount here because this is the "termination" packet. 28 if (done) { 30 29 progressDiv.append(` 31 30 <hr /> … … 38 37 `); 39 38 } else { 40 // 2. If NOT done, itmeans we definitely processed a user row.39 // If NOT done, means we definitely processed a user row. 41 40 if (skipped) { 42 41 skippedCount++; … … 47 46 } 48 47 49 // 3.Move to next offset and recurse48 // Move to next offset and recurse 50 49 offset++; 51 50 syncNextAuthor(); … … 63 62 64 63 function syncCategories(lastId = 0) { 65 // 1. Initialize the counter66 64 let syncedCount = 0; 67 65 66 progressDivCat.show(); 68 67 progressDivCat.html(` 69 68 <h3>Starting category sync...</h3> … … 81 80 82 81 if (done) { 83 // 3. Display the final count in the summary84 82 progressDivCat.append(` 85 83 <hr /> … … 91 89 `); 92 90 } else { 93 // 2. Increment the counter94 91 syncedCount++; 95 92 … … 109 106 110 107 function syncTags(lastId = 0) { 111 // 1. Initialize Sync Counter112 108 let syncedCount = 0; 113 109 114 progressDivTag.html(` 110 progressDivTag.show(); 111 progressDivTag.html(` 115 112 <h3>Starting tag sync...</h3> 116 113 <p style="font-weight: bold; color: #0073aa;">Please do NOT close this window until the sync is completed.</p> … … 126 123 const { message, done, last_id } = response.data; 127 124 128 // 2. Logic Check: Are we done?129 125 if (done) { 130 126 progressDivTag.append(` … … 137 133 `); 138 134 } else { 139 // 3.Not done: Increment count and display row135 // Not done: Increment count and display row 140 136 syncedCount++; 141 137 progressDivTag.append(`<div style="color: purple;">[${syncedCount}] ${message}</div>`); 142 138 143 // 4.Recurse with new ID139 // Recurse with new ID 144 140 syncNextTag(last_id); 145 141 } … … 148 144 } 149 145 }).fail(function(xhr) { 150 // 5.Catch Network/Server Errors146 // Catch Network/Server Errors 151 147 progressDivTag.append(`<div style="color:red;">Network/Server Error: ${xhr.status} - ${xhr.statusText}</div>`); 152 148 }); … … 154 150 155 151 syncNextTag(lastId); 152 } 153 154 function syncArticles() { 155 const selectedType = $('input[name="bus_sync_post_type"]:checked').val(); 156 157 if (!selectedType) { 158 alert('Please select a post type.'); 159 return; 160 } 161 162 let syncedCount = 0; 163 progressDivArticle.show(); 164 progressDivArticle.html(` 165 <h3>Starting article sync (Recent First)...</h3> 166 <p><strong>Type:</strong> ${selectedType}</p> 167 <p style="font-weight: bold; color: #0073aa;">Please do NOT close this window.</p> 168 <hr /> 169 `); 170 171 // Recursion 172 function syncNextArticle(lastIdCursor) { 173 $.post(SyncAuthorsAjax.ajax_url, { 174 action: 'sync_articles', 175 last_id: lastIdCursor, 176 // Send array as the PHP backend expects 'post_types' array 177 post_types: [selectedType] 178 }, function (response) { 179 if (response.success && response.data) { 180 const { message, done, last_id } = response.data; 181 182 if (done) { 183 progressDivArticle.append(` 184 <hr /> 185 <h3>Article Sync Complete:</h3> 186 <ul> 187 <li><strong>${syncedCount}</strong> articles successfully synced.</li> 188 </ul> 189 <strong style="color: green;">Process finished.</strong> 190 `); 191 } else { 192 syncedCount++; 193 progressDivArticle.append(`<div style="color: #333;">[${syncedCount}] ${message}</div>`); 194 progressDivArticle.scrollTop(progressDivArticle[0].scrollHeight); 195 syncNextArticle(last_id); 196 } 197 } else { 198 progressDivArticle.append(`<div style="color:red;">Error: ${response.data || 'Unknown'}</div>`); 199 } 200 }).fail(function(xhr) { 201 progressDivArticle.append(`<div style="color:red;">Server Error: ${xhr.status} ${xhr.statusText}</div>`); 202 }); 203 } 204 205 syncNextArticle(0); 156 206 } 157 207 … … 167 217 syncTags(); 168 218 }); 219 220 $('#sync-articles-button').on('click', function () { 221 syncArticles(); 222 }); 169 223 }); -
ringier-bus/trunk/readme.txt
r3412229 r3447091 4 4 Requires at least: 6.0 5 5 Tested up to: 6.9 6 Stable tag: 3. 4.16 Stable tag: 3.5.0 7 7 Requires PHP: 8.1 8 8 License: GPLv2 or later … … 161 161 162 162 == Changelog == 163 164 ### [3.5.0] - 2026-01-21 ### 165 166 #### Added #### 167 * (UI) **Batch Article Sync** tool added to the "BUS Tooling Page". 168 * Features a "Recent First" sync strategy to prioritize the newest content. 169 * Includes a Post Type selector (Radio button) to allow syncing specific custom post types. 170 * Displays real-time progress logs in the admin dashboard. 171 * (code) Implemented a **Reverse ID Cursor** strategy for the Article Sync logic. 172 * Ensures **O(1)** constant performance regardless of database size (efficiently handles 10k+ posts). 173 * Replaces standard `OFFSET` pagination to prevent timeout issues on deep database queries. 174 175 #### Changed #### 176 * (code) Major refactor of the `ArticleEvent` class to remove 3rd-party dependencies in favor of 100% native WP code: 177 * Removed `GuzzleHttp\Client` in favor of native `wp_remote_post()`. 178 * Removed `AuthenticationInterface` dependency; now uses `BusTokenManager` directly. 179 163 180 164 181 ### [3.4.0] - 2025-07-02 ### -
ringier-bus/trunk/ringier-bus.php
r3412229 r3447091 11 11 * Plugin URI: https://github.com/RingierIMU/mkt-plugin-wordpress-bus 12 12 * Description: A plugin to push events to Ringier CDE via the BUS API whenever an article is created, updated or deleted 13 * Version: 3. 4.113 * Version: 3.5.0 14 14 * Requires at least: 6.0 15 15 * Author: Ringier SA, Wasseem Khayrattee … … 50 50 */ 51 51 define('RINGIER_BUS_DS', DIRECTORY_SEPARATOR); 52 define('RINGIER_BUS_PLUGIN_VERSION', '3. 4.1');52 define('RINGIER_BUS_PLUGIN_VERSION', '3.5.0'); 53 53 define('RINGIER_BUS_PLUGIN_MINIMUM_WP_VERSION', '6.0'); 54 54 define('RINGIER_BUS_PLUGIN_DIR_URL', plugin_dir_url(__FILE__)); //has trailing slash at end -
ringier-bus/trunk/src/Core/AdminSyncPage.php
r3412229 r3447091 175 175 176 176 try { 177 \RingierBusPlugin\Bus\BusHelper::triggerTermCreatedEvent(177 BusHelper::triggerTermCreatedEvent( 178 178 $next_term->term_id, 179 179 $next_term->term_taxonomy_id, … … 225 225 226 226 try { 227 \RingierBusPlugin\Bus\BusHelper::triggerTermCreatedEvent(227 BusHelper::triggerTermCreatedEvent( 228 228 $next_term->term_id, 229 229 $next_term->term_taxonomy_id, … … 241 241 } 242 242 } 243 244 /** 245 * Article Sync 246 * Efficiently syncs articles (most recent first) using a Reverse ID Cursor strategy. 247 * 248 * Unlike standard OFFSET pagination which degrades in performance (O(N)), 249 * this method queries for the next ID smaller than the previous cursor (`WHERE ID < $last_id`). 250 * This guarantees O(1) constant performance for every request, even on massive datasets. 251 */ 252 public static function handleArticlesSync(): void 253 { 254 global $wpdb; 255 256 // Get parameters 257 $last_id = isset($_POST['last_id']) ? (int) $_POST['last_id'] : 0; 258 // array - flexible to accommodate checkboxes in future, but fallback to default 'post' 259 $post_types = isset($_POST['post_types']) ? array_map('sanitize_text_field', $_POST['post_types']) : ['post']; 260 261 // Prepare SQL for Reverse Cursor (Newest First) 262 $placeholders = implode(',', array_fill(0, count($post_types), '%s')); 263 264 $where_clause = "post_status = 'publish' AND post_type IN ($placeholders)"; 265 $args = $post_types; 266 267 if ($last_id > 0) { 268 $where_clause .= ' AND ID < %d'; 269 $args[] = $last_id; 270 } 271 272 // Fetch exactly ONE ID 273 $next_post_id = $wpdb->get_var($wpdb->prepare( 274 "SELECT ID FROM {$wpdb->posts} 275 WHERE $where_clause 276 ORDER BY ID DESC 277 LIMIT 1", 278 $args 279 )); 280 281 // Termination Condition 282 if (!$next_post_id) { 283 wp_send_json_success([ 284 'message' => 'All articles have been synced.', 285 'done' => true, 286 ]); 287 } 288 289 // Fetch Object & Dispatch 290 $post_object = get_post($next_post_id); 291 292 if (!$post_object) { 293 // Should not happen, but safe fallback 294 wp_send_json_error("Could not load post object for ID $next_post_id"); 295 } 296 297 try { 298 $success = BusHelper::dispatchArticlesEvent( 299 $post_object->ID, 300 $post_object 301 ); 302 303 if ($success) { 304 wp_send_json_success([ 305 'message' => "Synced Article (ID {$post_object->ID}) – " . mb_strimwidth($post_object->post_title, 0, 40, '...'), 306 'done' => false, 307 'last_id' => $post_object->ID, 308 ]); 309 } else { 310 // If false, it likely failed auth or validation inside the helper 311 wp_send_json_success([ 312 'message' => "Failed to sync Article (ID {$post_object->ID}) - Check logs. Moving to next..", 313 'done' => false, // We continue the loop even if one fails 314 'last_id' => $post_object->ID, 315 ]); 316 } 317 318 } catch (\Throwable $e) { 319 wp_send_json_error("Error ID {$next_post_id}: " . $e->getMessage()); 320 } 321 } 243 322 } -
ringier-bus/trunk/src/Core/Bus/BusHelper.php
r3412229 r3447091 859 859 $result = $busToken->acquireToken(); 860 860 if (!$result) { 861 ringier_errorlogthis($event_type . ': Failed to acquire BUS token ');862 Utils::pushToSlack("[{$event_type}] Failed to acquire BUS token ", Enum::LOG_ERROR);861 ringier_errorlogthis($event_type . ': Failed to acquire BUS token - try flushing the Auth token.'); 862 Utils::pushToSlack("[{$event_type}] Failed to acquire BUS token - try flushing the Auth token", Enum::LOG_ERROR); 863 863 864 864 return false; … … 871 871 return true; 872 872 } 873 874 /** 875 * Dispatch an Article event to the BUS. 876 * Used by Batch Sync tools and immediate triggers 877 * 878 * @param int $post_id 879 * @param WP_Post $post 880 * @param string $event_type 881 * 882 * @return bool 883 */ 884 public static function dispatchArticlesEvent(int $post_id, WP_Post $post, string $event_type = Enum::EVENT_ARTICLE_CREATED): bool 885 { 886 $endpointUrl = $_ENV[Enum::ENV_BUS_ENDPOINT] ?? ''; 887 888 if (empty($endpointUrl)) { 889 $msg = "ArticleEvent ($post_id): endpointUrl is empty"; 890 ringier_errorlogthis($msg); 891 Utils::pushToSlack($msg, Enum::LOG_ERROR); 892 893 return false; 894 } 895 896 $busToken = new BusTokenManager(); 897 $busToken->setParameters( 898 $endpointUrl, 899 $_ENV[Enum::ENV_VENTURE_CONFIG] ?? '', 900 $_ENV[Enum::ENV_BUS_API_USERNAME] ?? '', 901 $_ENV[Enum::ENV_BUS_API_PASSWORD] ?? '' 902 ); 903 904 // Acquire Token 905 if (!$busToken->acquireToken()) { 906 $msg = "ArticleEvent ($post_id): Failed to acquire BUS token"; 907 ringier_errorlogthis($msg); 908 Utils::pushToSlack($msg, Enum::LOG_ERROR); 909 910 return false; 911 } 912 913 $articlesEvent = new ArticlesEvent($busToken, $endpointUrl); 914 $articlesEvent->setEventType($event_type); 915 916 // Handle Brand Settings if available (mainly for ringier internal blogs) 917 if (class_exists('Brand_settings')) { 918 $articlesEvent->brandSettings = new \Brand_settings(); 919 } 920 921 try { 922 $articlesEvent->sendToBus($post_id, $post); 923 924 return true; 925 } catch (\Throwable $e) { 926 ringier_errorlogthis("ArticleEvent ($post_id) Exception: " . $e->getMessage()); 927 928 return false; 929 } 930 } 873 931 } -
ringier-bus/trunk/src/Core/BusPluginClass.php
r3316910 r3447091 73 73 add_action('wp_ajax_sync_categories', [AdminSyncPage::class, 'handleCategoriesSync']); 74 74 add_action('wp_ajax_sync_tags', [AdminSyncPage::class, 'handleTagsSync']); 75 add_action('wp_ajax_sync_articles', [AdminSyncPage::class, 'handleArticlesSync']); 75 76 76 77 // Handle custom POST -
ringier-bus/trunk/src/Core/Utils.php
r3412229 r3447091 301 301 // Build the formatted message 302 302 $slackMessage = sprintf( 303 "*%s* (%s):\n ```%s```",303 "*%s* (%s):\n %s ", 304 304 mb_strtoupper($level), 305 305 $location, -
ringier-bus/trunk/views/admin/admin-sync-page.php
r3316910 r3447091 22 22 <h2>Cache Flush Tools</h2> 23 23 24 <?php25 if (isset($_GET['flush_success']) && $_GET['flush_success'] === '1') {26 echo '<div class="updated notice is-dismissible"><p>The Auth token transient has been flushed.</p></div>';27 }24 <?php 25 if (isset($_GET['flush_success']) && $_GET['flush_success'] === '1') { 26 echo '<div class="updated notice is-dismissible"><p>The Auth token transient has been flushed.</p></div>'; 27 } 28 28 Utils::load_tpl(RINGIER_BUS_PLUGIN_VIEWS . 'admin/button-flush-transient.php'); 29 29 ?> … … 52 52 <button id="sync-authors-button" class="button">Sync All Authors</button> 53 53 54 <div id="sync-progress" style="margin-top: 20px; "><!-- Placeholder for AJAX process --></div>54 <div id="sync-progress" style="margin-top: 20px; max-height: 300px; overflow-y: auto; background: #f9f9f9; padding: 10px; border: 1px solid #ddd; display:none;"><!-- Placeholder for AJAX process --></div> 55 55 56 56 <div style="margin-bottom: 10px;"> </div> … … 62 62 <button id="sync-categories-button" class="button button-secondary">Sync All Categories</button> 63 63 64 <div id="sync-progress-cat" style="margin-top: 20px; "><!-- Placeholder for AJAX process --></div>64 <div id="sync-progress-cat" style="margin-top: 20px; max-height: 300px; overflow-y: auto; background: #f9f9f9; padding: 10px; border: 1px solid #ddd; display:none;"><!-- Placeholder for AJAX process --></div> 65 65 66 66 <div style="margin-bottom: 10px;"> </div> … … 72 72 <button id="sync-tags-button" class="button">Sync All Tags</button> 73 73 74 <div id="sync-progress-tag" style="margin-top: 20px;"><!-- Placeholder for AJAX process --></div> 74 <div id="sync-progress-tag" style="margin-top: 20px; max-height: 300px; overflow-y: auto; background: #f9f9f9; padding: 10px; border: 1px solid #ddd; display:none;"><!-- Placeholder for AJAX process --></div> 75 76 <div style="margin-bottom: 10px;"> </div> 77 <hr /> 78 79 <h2>Article Sync</h2> 80 <p> 81 Syncs content starting from the <strong>most recent</strong>. 82 Select the Post Type you wish to sync: 83 </p> 84 85 <?php 86 // Fetch all public post types 87 $post_types = get_post_types(['public' => true], 'objects'); 88 $exclude = [ 89 'attachment', 90 'elementor_library', 91 ]; // todo: Add more types to exclude 92 ?> 93 94 <div class="post-types-selector" style="margin-bottom: 15px; background: #fff; padding: 15px; border: 1px solid #ccd0d4;"> 95 <?php foreach ($post_types as $pt): ?> 96 <?php if (in_array($pt->name, $exclude)) { 97 continue; 98 } ?> 99 <label style="margin-right: 15px; display: inline-block; margin-bottom: 5px;"> 100 <input type="radio" name="bus_sync_post_type" value="<?php echo esc_attr($pt->name); ?>" 101 <?php checked($pt->name, 'post'); // Default to 'post' checked?> /> 102 <?php echo esc_html($pt->label); ?> (<code><?php echo esc_html($pt->name); ?></code>) 103 </label> 104 <?php endforeach; ?> 105 </div> 106 107 <button id="sync-articles-button" class="button">Sync Selected Articles</button> 108 109 <div id="sync-progress-article" style="margin-top: 20px; max-height: 300px; overflow-y: auto; background: #f9f9f9; padding: 10px; border: 1px solid #ddd; display:none;"> 110 </div> 75 111 76 112 <div style="margin-bottom: 10px;"> </div> -
ringier-bus/trunk/views/admin/button-flush-transient.php
r3316910 r3447091 2 2 <?php wp_nonce_field('flush_transients_nonce'); ?> 3 3 <input type="hidden" name="action" value="flush_all_transients"> 4 <button type="submit" class="button button- secondary">FlushAuth Token</button>4 <button type="submit" class="button button-primary">Flush API Auth Token</button> 5 5 </form>
Note: See TracChangeset
for help on using the changeset viewer.