Plugin Directory

Changeset 3447091


Ignore:
Timestamp:
01/26/2026 12:50:47 PM (2 months ago)
Author:
ringier
Message:

chore: commit changes for v3.5.0 - see CHANGELOG.md

Location:
ringier-bus/trunk
Files:
1 added
11 edited

Legend:

Unmodified
Added
Removed
  • ringier-bus/trunk/CHANGELOG.md

    r3412229 r3447091  
    1313* (dependency) Fully remove Guzzle/Symfony dependencies
    1414* (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.
    1532
    1633
  • ringier-bus/trunk/README.md

    r3412229 r3447091  
    77**Requires at least:** 6.0 
    88**Tested up to:** 6.9 
    9 **Stable tag:** 3.4.1 
     9**Stable tag:** 3.5.0 
    1010**Requires PHP:** 8.1 
    1111**License:** GPLv2 or later 
  • ringier-bus/trunk/assets/js/sync-tools.js

    r3412229 r3447091  
    33    const progressDivCat = $('#sync-progress-cat');
    44    const progressDivTag = $('#sync-progress-tag');
     5    const progressDivArticle = $('#sync-progress-article');
    56
    67    function syncAuthors() {
     
    910        let skippedCount = 0;
    1011
     12        progressDiv.show();
    1113        progressDiv.html(`
    1214            <h3>Starting to sync all authors having the following roles:</h3>
     
    2426                    const { message, done, skipped } = response.data;
    2527
    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) {
    3029                        progressDiv.append(`
    3130                            <hr />
     
    3837                        `);
    3938                    } else {
    40                         // 2. If NOT done, it means we definitely processed a user row.
     39                        // If NOT done, means we definitely processed a user row.
    4140                        if (skipped) {
    4241                            skippedCount++;
     
    4746                        }
    4847
    49                         // 3. Move to next offset and recurse
     48                        // Move to next offset and recurse
    5049                        offset++;
    5150                        syncNextAuthor();
     
    6362
    6463    function syncCategories(lastId = 0) {
    65         // 1. Initialize the counter
    6664        let syncedCount = 0;
    6765
     66        progressDivCat.show();
    6867        progressDivCat.html(`
    6968        <h3>Starting category sync...</h3>
     
    8180
    8281                    if (done) {
    83                         // 3. Display the final count in the summary
    8482                        progressDivCat.append(`
    8583                        <hr />
     
    9189                    `);
    9290                    } else {
    93                         // 2. Increment the counter
    9491                        syncedCount++;
    9592
     
    109106
    110107    function syncTags(lastId = 0) {
    111         // 1. Initialize Sync Counter
    112108        let syncedCount = 0;
    113109
    114             progressDivTag.html(`
     110        progressDivTag.show();
     111        progressDivTag.html(`
    115112            <h3>Starting tag sync...</h3>
    116113            <p style="font-weight: bold; color: #0073aa;">Please do NOT close this window until the sync is completed.</p>
     
    126123                    const { message, done, last_id } = response.data;
    127124
    128                     // 2. Logic Check: Are we done?
    129125                    if (done) {
    130126                        progressDivTag.append(`
     
    137133                    `);
    138134                    } else {
    139                         // 3. Not done: Increment count and display row
     135                        // Not done: Increment count and display row
    140136                        syncedCount++;
    141137                        progressDivTag.append(`<div style="color: purple;">[${syncedCount}] ${message}</div>`);
    142138
    143                         // 4. Recurse with new ID
     139                        // Recurse with new ID
    144140                        syncNextTag(last_id);
    145141                    }
     
    148144                }
    149145            }).fail(function(xhr) {
    150                 // 5. Catch Network/Server Errors
     146                // Catch Network/Server Errors
    151147                progressDivTag.append(`<div style="color:red;">Network/Server Error: ${xhr.status} - ${xhr.statusText}</div>`);
    152148            });
     
    154150
    155151        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);
    156206    }
    157207
     
    167217        syncTags();
    168218    });
     219
     220    $('#sync-articles-button').on('click', function () {
     221        syncArticles();
     222    });
    169223});
  • ringier-bus/trunk/readme.txt

    r3412229 r3447091  
    44Requires at least: 6.0
    55Tested up to: 6.9
    6 Stable tag: 3.4.1
     6Stable tag: 3.5.0
    77Requires PHP: 8.1
    88License: GPLv2 or later
     
    161161
    162162== 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
    163180
    164181### [3.4.0] - 2025-07-02 ###
  • ringier-bus/trunk/ringier-bus.php

    r3412229 r3447091  
    1111 * Plugin URI: https://github.com/RingierIMU/mkt-plugin-wordpress-bus
    1212 * 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.1
     13 * Version: 3.5.0
    1414 * Requires at least: 6.0
    1515 * Author: Ringier SA, Wasseem Khayrattee
     
    5050 */
    5151define('RINGIER_BUS_DS', DIRECTORY_SEPARATOR);
    52 define('RINGIER_BUS_PLUGIN_VERSION', '3.4.1');
     52define('RINGIER_BUS_PLUGIN_VERSION', '3.5.0');
    5353define('RINGIER_BUS_PLUGIN_MINIMUM_WP_VERSION', '6.0');
    5454define('RINGIER_BUS_PLUGIN_DIR_URL', plugin_dir_url(__FILE__)); //has trailing slash at end
  • ringier-bus/trunk/src/Core/AdminSyncPage.php

    r3412229 r3447091  
    175175
    176176        try {
    177             \RingierBusPlugin\Bus\BusHelper::triggerTermCreatedEvent(
     177            BusHelper::triggerTermCreatedEvent(
    178178                $next_term->term_id,
    179179                $next_term->term_taxonomy_id,
     
    225225
    226226        try {
    227             \RingierBusPlugin\Bus\BusHelper::triggerTermCreatedEvent(
     227            BusHelper::triggerTermCreatedEvent(
    228228                $next_term->term_id,
    229229                $next_term->term_taxonomy_id,
     
    241241        }
    242242    }
     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    }
    243322}
  • ringier-bus/trunk/src/Core/Bus/BusHelper.php

    r3412229 r3447091  
    859859        $result = $busToken->acquireToken();
    860860        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);
    863863
    864864            return false;
     
    871871        return true;
    872872    }
     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    }
    873931}
  • ringier-bus/trunk/src/Core/BusPluginClass.php

    r3316910 r3447091  
    7373        add_action('wp_ajax_sync_categories', [AdminSyncPage::class, 'handleCategoriesSync']);
    7474        add_action('wp_ajax_sync_tags', [AdminSyncPage::class, 'handleTagsSync']);
     75        add_action('wp_ajax_sync_articles', [AdminSyncPage::class, 'handleArticlesSync']);
    7576
    7677        // Handle custom POST
  • ringier-bus/trunk/src/Core/Utils.php

    r3412229 r3447091  
    301301        // Build the formatted message
    302302        $slackMessage = sprintf(
    303             "*%s* (%s):\n```%s```",
     303            "*%s* (%s):\n %s ",
    304304            mb_strtoupper($level),
    305305            $location,
  • ringier-bus/trunk/views/admin/admin-sync-page.php

    r3316910 r3447091  
    2222    <h2>Cache Flush Tools</h2>
    2323
    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     }
     24<?php
     25if (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}
    2828Utils::load_tpl(RINGIER_BUS_PLUGIN_VIEWS . 'admin/button-flush-transient.php');
    2929?>
     
    5252    <button id="sync-authors-button" class="button">Sync All Authors</button>
    5353
    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>
    5555
    5656    <div style="margin-bottom: 10px;">&nbsp;</div>
     
    6262    <button id="sync-categories-button" class="button button-secondary">Sync All Categories</button>
    6363
    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>
    6565
    6666    <div style="margin-bottom: 10px;">&nbsp;</div>
     
    7272    <button id="sync-tags-button" class="button">Sync All Tags</button>
    7373
    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;">&nbsp;</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>
    75111
    76112    <div style="margin-bottom: 10px;">&nbsp;</div>
  • ringier-bus/trunk/views/admin/button-flush-transient.php

    r3316910 r3447091  
    22    <?php wp_nonce_field('flush_transients_nonce'); ?>
    33    <input type="hidden" name="action" value="flush_all_transients">
    4     <button type="submit" class="button button-secondary">Flush Auth Token</button>
     4    <button type="submit" class="button button-primary">Flush API Auth Token</button>
    55</form>
Note: See TracChangeset for help on using the changeset viewer.