Plugin Directory

Changeset 3423068


Ignore:
Timestamp:
12/18/2025 03:56:47 PM (3 months ago)
Author:
Atastic
Message:

Version 1.0.7: Add Trash Matching Posts feature to bulk-trash 410 posts

Location:
410-link-unlinker
Files:
2 edited
3 copied

Legend:

Unmodified
Added
Removed
  • 410-link-unlinker/tags/1.0.7/410-link-unlinker.php

    r3422890 r3423068  
    33 * Plugin Name: 410 Link Unlinker
    44 * Description: Removes anchor wrappers (<a href="…">…</a>) in posts/pages (and optionally postmeta) when links match a provided list (e.g., 410 Gone). Keeps the visible text.
    5  * Version:     1.0.6
     5 * Version:     1.0.7
    66 * Author:      Arnjen
    77 * License:     GPL-2.0-or-later
     
    1717    const OPT_KEY = 'link410_urls';
    1818    const NONCE   = 'link410_nonce';
    19     const VER     = '1.0.6';
     19    const VER     = '1.0.7';
    2020
    2121    public function __construct() {
     
    8585                <p><button class="button button-secondary" name="link410_action" value="save"><?php esc_html_e('Save URL list', '410-link-unlinker'); ?></button></p>
    8686
    87                 <h2><?php esc_html_e('2) Run', '410-link-unlinker'); ?></h2>
     87                <h2><?php esc_html_e('2) Remove Links', '410-link-unlinker'); ?></h2>
    8888                <p><label><input type="checkbox" name="link410_include_meta" value="1"> <?php esc_html_e('Also process wp_postmeta', '410-link-unlinker'); ?></label></p>
    8989                <p>
     
    9898                    <button class="button button-primary" name="link410_action" value="run"><?php esc_html_e('Scan / Unlink Now', '410-link-unlinker'); ?></button>
    9999                </p>
     100
     101                <h2><?php esc_html_e('3) Trash Matching Posts', '410-link-unlinker'); ?></h2>
     102                <p><?php esc_html_e('Find and trash posts whose URL/slug matches your 410 list. This removes them from author pages, archives, etc.', '410-link-unlinker'); ?></p>
     103                <p>
     104                    <button class="button button-secondary" name="link410_action" value="trash"><?php esc_html_e('Find & Trash Matching Posts', '410-link-unlinker'); ?></button>
     105                </p>
    100106            </form>
    101107        </div>
     
    159165                    echo '<div class="notice notice-info"><p><strong>Examples:</strong></p><ol>';
    160166                    foreach ($result['examples'] as $ex) { echo '<li>'.esc_html($ex).'</li>'; }
     167                    echo '</ol></div>';
     168                }
     169            });
     170        }
     171
     172        if ($action === 'trash') {
     173            $dry = !empty($_POST['link410_dry']);
     174            $urls = $this->get_saved_urls();
     175            if (!$urls) {
     176                add_action('admin_notices', function(){ echo '<div class="notice notice-error"><p>'.esc_html__('No URLs saved. Please add your 410 list and try again.', '410-link-unlinker').'</p></div>'; });
     177                return;
     178            }
     179
     180            $result = $this->trash_matching_posts($urls, $dry);
     181            add_action('admin_notices', function() use ($result, $dry){
     182                $mode = $dry ? __('Dry-run', '410-link-unlinker') : __('Trashed', '410-link-unlinker');
     183                $msg = sprintf('%s: %d posts found matching your 410 URLs.', $mode, $result['count']);
     184                echo '<div class="notice notice-success"><p>'.esc_html($msg).'</p></div>';
     185                if (!empty($result['posts'])) {
     186                    echo '<div class="notice notice-info"><p><strong>'.($dry ? 'Posts that would be trashed:' : 'Trashed posts:').'</strong></p><ol>';
     187                    foreach ($result['posts'] as $p) { echo '<li>'.esc_html($p).'</li>'; }
    161188                    echo '</ol></div>';
    162189                }
     
    291318    }
    292319
     320    private function trash_matching_posts(array $urls, bool $dry) {
     321        global $wpdb;
     322
     323        // Extract slugs from URLs
     324        $slugs = [];
     325        foreach ($urls as $u) {
     326            // Parse the URL to get the path
     327            if (strpos($u, '://') !== false) {
     328                $p = wp_parse_url($u);
     329                $path = isset($p['path']) ? $p['path'] : '';
     330            } else {
     331                $parts = explode('/', $u, 2);
     332                $path = isset($parts[1]) ? '/'.$parts[1] : '';
     333            }
     334            // Get the last segment of the path as the slug
     335            $path = rtrim($path, '/');
     336            if ($path) {
     337                $segments = explode('/', $path);
     338                $slug = end($segments);
     339                if ($slug && $slug !== '') {
     340                    $slugs[] = $slug;
     341                }
     342            }
     343        }
     344
     345        if (empty($slugs)) {
     346            return ['count' => 0, 'posts' => []];
     347        }
     348
     349        $slugs = array_unique($slugs);
     350
     351        // Find posts matching these slugs
     352        $placeholders = implode(',', array_fill(0, count($slugs), '%s'));
     353        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Bulk operation
     354        $posts = $wpdb->get_results(
     355            $wpdb->prepare(
     356                "SELECT ID, post_title, post_name, post_type FROM {$wpdb->posts} WHERE post_name IN ($placeholders) AND post_status = 'publish'",
     357                ...$slugs
     358            ),
     359            ARRAY_A
     360        );
     361
     362        $result = ['count' => 0, 'posts' => []];
     363
     364        foreach ((array)$posts as $post) {
     365            $result['count']++;
     366            $result['posts'][] = $post['post_title'] . ' (/' . $post['post_name'] . '/) [' . $post['post_type'] . ']';
     367
     368            if (!$dry) {
     369                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Bulk operation
     370                $wpdb->update(
     371                    $wpdb->posts,
     372                    ['post_status' => 'trash'],
     373                    ['ID' => (int)$post['ID']]
     374                );
     375                clean_post_cache((int)$post['ID']);
     376            }
     377        }
     378
     379        return $result;
     380    }
     381
    293382    private function build_patterns(array $urls) {
    294383        $esc = static function($s){ return preg_quote($s, '/'); };
  • 410-link-unlinker/tags/1.0.7/readme.txt

    r3422890 r3423068  
    55Tested up to: 6.9
    66Requires PHP: 7.4
    7 Stable tag: 1.0.6
     7Stable tag: 1.0.7
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    2424
    2525== Changelog ==
     26= 1.0.7 =
     27* Add "Trash Matching Posts" feature to bulk-trash posts whose URLs match your 410 list (removes them from author pages, archives, etc.).
     28
    2629= 1.0.6 =
    2730* Fix URL parsing to accept URLs without https:// prefix.
  • 410-link-unlinker/trunk/410-link-unlinker.php

    r3422890 r3423068  
    33 * Plugin Name: 410 Link Unlinker
    44 * Description: Removes anchor wrappers (<a href="…">…</a>) in posts/pages (and optionally postmeta) when links match a provided list (e.g., 410 Gone). Keeps the visible text.
    5  * Version:     1.0.6
     5 * Version:     1.0.7
    66 * Author:      Arnjen
    77 * License:     GPL-2.0-or-later
     
    1717    const OPT_KEY = 'link410_urls';
    1818    const NONCE   = 'link410_nonce';
    19     const VER     = '1.0.6';
     19    const VER     = '1.0.7';
    2020
    2121    public function __construct() {
     
    8585                <p><button class="button button-secondary" name="link410_action" value="save"><?php esc_html_e('Save URL list', '410-link-unlinker'); ?></button></p>
    8686
    87                 <h2><?php esc_html_e('2) Run', '410-link-unlinker'); ?></h2>
     87                <h2><?php esc_html_e('2) Remove Links', '410-link-unlinker'); ?></h2>
    8888                <p><label><input type="checkbox" name="link410_include_meta" value="1"> <?php esc_html_e('Also process wp_postmeta', '410-link-unlinker'); ?></label></p>
    8989                <p>
     
    9898                    <button class="button button-primary" name="link410_action" value="run"><?php esc_html_e('Scan / Unlink Now', '410-link-unlinker'); ?></button>
    9999                </p>
     100
     101                <h2><?php esc_html_e('3) Trash Matching Posts', '410-link-unlinker'); ?></h2>
     102                <p><?php esc_html_e('Find and trash posts whose URL/slug matches your 410 list. This removes them from author pages, archives, etc.', '410-link-unlinker'); ?></p>
     103                <p>
     104                    <button class="button button-secondary" name="link410_action" value="trash"><?php esc_html_e('Find & Trash Matching Posts', '410-link-unlinker'); ?></button>
     105                </p>
    100106            </form>
    101107        </div>
     
    159165                    echo '<div class="notice notice-info"><p><strong>Examples:</strong></p><ol>';
    160166                    foreach ($result['examples'] as $ex) { echo '<li>'.esc_html($ex).'</li>'; }
     167                    echo '</ol></div>';
     168                }
     169            });
     170        }
     171
     172        if ($action === 'trash') {
     173            $dry = !empty($_POST['link410_dry']);
     174            $urls = $this->get_saved_urls();
     175            if (!$urls) {
     176                add_action('admin_notices', function(){ echo '<div class="notice notice-error"><p>'.esc_html__('No URLs saved. Please add your 410 list and try again.', '410-link-unlinker').'</p></div>'; });
     177                return;
     178            }
     179
     180            $result = $this->trash_matching_posts($urls, $dry);
     181            add_action('admin_notices', function() use ($result, $dry){
     182                $mode = $dry ? __('Dry-run', '410-link-unlinker') : __('Trashed', '410-link-unlinker');
     183                $msg = sprintf('%s: %d posts found matching your 410 URLs.', $mode, $result['count']);
     184                echo '<div class="notice notice-success"><p>'.esc_html($msg).'</p></div>';
     185                if (!empty($result['posts'])) {
     186                    echo '<div class="notice notice-info"><p><strong>'.($dry ? 'Posts that would be trashed:' : 'Trashed posts:').'</strong></p><ol>';
     187                    foreach ($result['posts'] as $p) { echo '<li>'.esc_html($p).'</li>'; }
    161188                    echo '</ol></div>';
    162189                }
     
    291318    }
    292319
     320    private function trash_matching_posts(array $urls, bool $dry) {
     321        global $wpdb;
     322
     323        // Extract slugs from URLs
     324        $slugs = [];
     325        foreach ($urls as $u) {
     326            // Parse the URL to get the path
     327            if (strpos($u, '://') !== false) {
     328                $p = wp_parse_url($u);
     329                $path = isset($p['path']) ? $p['path'] : '';
     330            } else {
     331                $parts = explode('/', $u, 2);
     332                $path = isset($parts[1]) ? '/'.$parts[1] : '';
     333            }
     334            // Get the last segment of the path as the slug
     335            $path = rtrim($path, '/');
     336            if ($path) {
     337                $segments = explode('/', $path);
     338                $slug = end($segments);
     339                if ($slug && $slug !== '') {
     340                    $slugs[] = $slug;
     341                }
     342            }
     343        }
     344
     345        if (empty($slugs)) {
     346            return ['count' => 0, 'posts' => []];
     347        }
     348
     349        $slugs = array_unique($slugs);
     350
     351        // Find posts matching these slugs
     352        $placeholders = implode(',', array_fill(0, count($slugs), '%s'));
     353        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Bulk operation
     354        $posts = $wpdb->get_results(
     355            $wpdb->prepare(
     356                "SELECT ID, post_title, post_name, post_type FROM {$wpdb->posts} WHERE post_name IN ($placeholders) AND post_status = 'publish'",
     357                ...$slugs
     358            ),
     359            ARRAY_A
     360        );
     361
     362        $result = ['count' => 0, 'posts' => []];
     363
     364        foreach ((array)$posts as $post) {
     365            $result['count']++;
     366            $result['posts'][] = $post['post_title'] . ' (/' . $post['post_name'] . '/) [' . $post['post_type'] . ']';
     367
     368            if (!$dry) {
     369                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Bulk operation
     370                $wpdb->update(
     371                    $wpdb->posts,
     372                    ['post_status' => 'trash'],
     373                    ['ID' => (int)$post['ID']]
     374                );
     375                clean_post_cache((int)$post['ID']);
     376            }
     377        }
     378
     379        return $result;
     380    }
     381
    293382    private function build_patterns(array $urls) {
    294383        $esc = static function($s){ return preg_quote($s, '/'); };
  • 410-link-unlinker/trunk/readme.txt

    r3422890 r3423068  
    55Tested up to: 6.9
    66Requires PHP: 7.4
    7 Stable tag: 1.0.6
     7Stable tag: 1.0.7
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    2424
    2525== Changelog ==
     26= 1.0.7 =
     27* Add "Trash Matching Posts" feature to bulk-trash posts whose URLs match your 410 list (removes them from author pages, archives, etc.).
     28
    2629= 1.0.6 =
    2730* Fix URL parsing to accept URLs without https:// prefix.
Note: See TracChangeset for help on using the changeset viewer.