Plugin Directory

Changeset 3475895


Ignore:
Timestamp:
03/05/2026 08:03:50 PM (3 weeks ago)
Author:
apos37
Message:

1.3.7

  • Update: Changed storage of broken links from custom post type to custom database table
  • Update: Add option to cleanup on uninstall
  • Fix: Error when blnotifier_link_before_prechecks hook returns status
Location:
broken-link-notifier
Files:
58 added
14 deleted
10 edited

Legend:

Unmodified
Added
Removed
  • broken-link-notifier/trunk/broken-link-notifier.php

    r3471187 r3475895  
    44 * Plugin URI:          https://pluginrx.com/plugin/broken-link-notifier/
    55 * Description:         Get notified when someone loads a page with a broken link
    6  * Version:             1.3.6
     6 * Version:             1.3.7
    77 * Requires at least:   5.9
    88 * Tested up to:        6.9
  • broken-link-notifier/trunk/includes/cache.php

    r3287974 r3475895  
    1818
    1919/**
    20  * Main plugin class.
     20 * Cache class.
    2121 */
    2222class BLNOTIFIER_CACHE {
  • broken-link-notifier/trunk/includes/example-hook.php

    r3067171 r3475895  
    33
    44/**
    5  * An example of how to filter a link before it's checked for validity
     5 * Skip links containing a specific query string before link checks.
    66 *
    7  * @param string $link
    8  * @return string|false
     7 * @param string|array $link The original link to be checked.
     8 * @return string|array Modified link or structured status array.
    99 */
    10 function blnotifier_link( $link ) {
    11     // Filter ajax calls
    12     if ( strpos( $link, '/admin-ajax.php' ) !== false ) {
    13 
    14         // Parse the url (ie. https://mydomain.com/wp-admin/admin-ajax.php?action=my_action&post_id=12345&nonce=1234567890)
    15         $url_components = wp_parse_url( $link );
    16         $query_strings = explode( '&', $url_components[ 'query' ] );
    17         $action = false;
    18         $post_id = false;
    19         foreach ( $query_strings as $query_string ) {
    20             $query_string = str_replace( 'amp;', '', $query_string );
    21             if ( substr( $query_string, 0, strlen( 'action=' ) ) == 'action=' ) {
    22                 $action = substr( $query_string, strlen( 'action=' ) );
    23             }
    24             if ( substr( $query_string, 0, strlen( 'post_id=' ) ) == 'post_id=' ) {
    25                 $post_id = substr( $query_string, strlen( 'post_id=' ) );
    26             }
    27         }
    28 
    29         // Check the action and make sure we have a post id
    30         if ( $action == 'my_action' && $post_id ) {
    31 
    32             // Check the post it's associated with
    33             $meta_value = get_post_meta( $post_id, 'meta_key', true );
    34 
    35             // New url to pass through all the checks
    36             if ( $meta_value && $meta_value == 'some keyword' ) {
    37                 $link = 'https://somewebsite.com/'.$meta_value;
    38             }
    39 
    40         // Or simply return a status
    41         } elseif ( $action == 'my_other_action' ) {
    42 
    43             // Condition
    44             if ( get_post( $post_id ) ) {
    45                 $status = [
    46                     'type' => 'good',
    47                     'code' => 200,
    48                     'text' => 'Post exists: '.get_the_title( $post_id ),
    49                     'link' => $link
    50                 ];
    51             } else {
    52                 $status = [
    53                     'type' => 'broken',
    54                     'code' => 666, // We use 666 as an alternative to 0 in case warnings are disabled
    55                     'text' => 'Post does not exist',
    56                     'link' => $link
    57                 ];
    58             }
    59             return $status;
    60         }
     10add_filter( 'blnotifier_link_before_prechecks', function( $link ) {
     11    // Skip UTM tracking links
     12    if ( is_string( $link ) && strpos( $link, 'utm_' ) !== false ) {
     13        return [
     14            'type' => 'good',
     15            'code' => 200,
     16            'text' => 'Skipped due to tracking params',
     17            'link' => $link
     18        ];
    6119    }
    6220
    63     // Always return link
    6421    return $link;
    65 } // End blnotifier_link()
    66 
    67 add_filter( 'blnotifier_link_before_prechecks', 'blnotifier_link' );
     22}, 10 );
  • broken-link-notifier/trunk/includes/helpers.php

    r3471187 r3475895  
    200200            return [];
    201201        }
    202 
    203         unset( $post_types[ ( new BLNOTIFIER_RESULTS )->post_type ] );
    204202
    205203        if ( isset( $post_types[ 'help-docs' ] ) ) {
     
    405403     */
    406404    public function count_broken_links() {
    407         $broken_links = get_posts( [
    408             'posts_per_page'    => -1,
    409             'post_status'       => 'publish',
    410             'post_type'         => 'blnotifier-results',
    411             'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
    412                 [
    413                     'key'   => 'type',
    414                     'value' => 'broken',
    415                 ]
    416             ],
    417             'fields' => 'ids'
    418         ] );
    419         return count( $broken_links );
     405        global $wpdb;
     406        $table = $wpdb->prefix . 'blnotifier_results';
     407
     408        $count = $wpdb->get_var(
     409            $wpdb->prepare(
     410                "SELECT COUNT(*) FROM {$table} WHERE type = %s",
     411                'broken'
     412            )
     413        );
     414
     415        return absint( $count );
    420416    } // End count_broken_links()
    421417
     
    15631559
    15641560        // String replace
    1565         $link = $this->str_replace_on_link( $link );
     1561        if ( is_array( $link ) && isset( $link[ 'link' ] ) ) {
     1562            $link[ 'link' ] = $this->str_replace_on_link( $link[ 'link' ] );
     1563        } elseif ( is_string( $link ) ) {
     1564            $link = $this->str_replace_on_link( $link );
     1565        }
    15661566
    15671567        // Check for cached link only if the link is not an array
     
    18001800     */
    18011801    public function get_broken_links() {
     1802        global $wpdb;
     1803        $table = $wpdb->prefix . 'blnotifier_results';
     1804
     1805        $rows = $wpdb->get_results(
     1806            $wpdb->prepare(
     1807                "SELECT link, text, type, code, source, location, method, author_id, created_at FROM {$table} WHERE type = %s ORDER BY created_at DESC",
     1808                'broken'
     1809            ),
     1810            ARRAY_A
     1811        );
     1812
    18021813        $links = [];
    1803 
    1804         $results = new BLNOTIFIER_RESULTS();
    1805         $post_type = $results->post_type;
    1806 
    1807         $posts = get_posts( [
    1808             'post_type'      => $post_type,
    1809             'posts_per_page' => -1,
    1810             'post_status'    => 'publish',
    1811             'orderby'        => 'date',
    1812             'order'          => 'DESC',
    1813         ] );
     1814        $source_cache = []; // stores [ 'post_id' => ..., 'title' => ... ] for each source URL
    18141815
    18151816        $methods = [
    1816             'visit'   => __( 'Front-End Visit', 'broken-link-notifier' ),
    1817             'multi'   => __( 'Multi-Scan', 'broken-link-notifier' ),
    1818             'single'  => __( 'Page Scan', 'broken-link-notifier' ),
     1817            'visit'  => __( 'Front-End Visit', 'broken-link-notifier' ),
     1818            'multi'  => __( 'Multi-Scan', 'broken-link-notifier' ),
     1819            'single' => __( 'Page Scan', 'broken-link-notifier' ),
    18191820        ];
    18201821
    1821         foreach ( $posts as $post ) {
    1822             $source_link = sanitize_url( get_post_meta( $post->ID, 'source', true ) );
    1823             $location = sanitize_key( get_post_meta( $post->ID, 'location', true ) );
    1824             $type_meta = sanitize_key( get_post_meta( $post->ID, 'type', true ) );
    1825             $code = absint( get_post_meta( $post->ID, 'code', true ) );
    1826             $user_id = $post->post_author;
    1827             $user = $user_id ? get_userdata( $user_id ) : null;
    1828             $method = sanitize_key( get_post_meta( $post->ID, 'method', true ) );
     1822        foreach ( $rows as $row ) {
     1823
     1824            $source_url = $row[ 'source' ];
     1825
     1826            if ( isset( $source_cache[ $source_url ] ) ) {
     1827                $source_post_id = $source_cache[ $source_url ][ 'post_id' ];
     1828                $source_title   = $source_cache[ $source_url ][ 'title' ];
     1829            } else {
     1830                $source_post_id = url_to_postid( $source_url );
     1831                $source_title   = $source_post_id ? get_the_title( $source_post_id ) : __( 'Unknown', 'broken-link-notifier' );
     1832                $source_cache[ $source_url ] = [
     1833                    'post_id' => $source_post_id,
     1834                    'title'   => $source_title,
     1835                ];
     1836            }
     1837
     1838            $user = $row[ 'author_id' ] ? get_userdata( $row[ 'author_id' ] ) : null;
    18291839
    18301840            $links[] = [
    1831                 'date'        => gmdate( 'Y-m-d H:i:s', strtotime( $post->post_date ) ),
    1832                 'type'        => ucwords( $type_meta ),
    1833                 'code'        => $code,
    1834                 'message'     => $post->post_content,
    1835                 'link'        => $post->post_title,
    1836                 'source_name' => $source_link ? get_the_title( url_to_postid( $source_link ) ) : 'Unknown',
    1837                 'source_link' => $source_link,
    1838                 'location'    => ucwords( $location ),
    1839                 'user'        => $user ? $user->display_name : 'Guest',
    1840                 'method'      => isset( $methods[ $method ] ) ? $methods[ $method ] : __( 'Unknown', 'broken-link-notifier' ),
     1841                'date'        => mysql2date( 'Y-m-d H:i:s', $row[ 'created_at' ] ),
     1842                'type'        => ucwords( sanitize_text_field( $row[ 'type' ] ) ),
     1843                'code'        => absint( $row[ 'code' ] ),
     1844                'message'     => sanitize_text_field( $row[ 'text' ] ),
     1845                'link'        => esc_url( $row[ 'link' ] ),
     1846                'source_name' => $source_title,
     1847                'source_link' => esc_url( $source_url ),
     1848                'location'    => ucwords( sanitize_key( $row[ 'location' ] ) ),
     1849                'user'        => $user ? $user->display_name : __( 'Guest', 'broken-link-notifier' ),
     1850                'method'      => isset( $methods[ $row[ 'method' ] ] ) ? $methods[ $row[ 'method' ] ] : __( 'Unknown', 'broken-link-notifier' ),
    18411851            ];
    18421852        }
     
    18441854        usort( $links, function( $a, $b ) {
    18451855            $date_compare = strtotime( $b[ 'date' ] ) <=> strtotime( $a[ 'date' ] );
    1846        
    18471856            if ( $date_compare === 0 ) {
    18481857                return strcasecmp( $a[ 'link' ], $b[ 'link' ] );
    18491858            }
    1850        
    18511859            return $date_compare;
    1852         } );
     1860        });
    18531861
    18541862        return $links;
  • broken-link-notifier/trunk/includes/js/omits.js

    r3067171 r3475895  
    77    // Scan type
    88    const scanType = blnotifier_omit.scan_type;
     9    const omitEl = scanType == 'scan-results' ? '.omit-link a' : '.omit-link';
    910
    1011    // Listen for omitting links
    11     $( '.omit-link' ).on( 'click', function( e ) {
     12    $( omitEl ).on( 'click', function( e ) {
    1213        e.preventDefault();
    1314        var row;
    1415        var link;
    1516        if ( scanType == 'scan-results' ) {
    16             const postID = $( this ).data( 'post-id' );
    17             $( `#post-${postID}` ).addClass( 'omitted' );
     17            const linkID = $( this ).closest( 'tr' ).data( 'link-id' );
     18            $( `#link-${linkID}` ).addClass( 'omitted' );
    1819            $( this ).replaceWith( 'Omitted' );
    1920            link = $( this ).data( 'link' );
  • broken-link-notifier/trunk/includes/js/results-back.js

    r3258065 r3475895  
    11jQuery( $ => {
    22    // console.log( 'Broken Link Notifier JS Loaded...' );
    3 
    4     // Add target _blank to title links
    5     $( 'a.row-title' ).attr( 'target', '_blank' );
    6 
    7     // Clear filters
    8     $( '#link-type-filter' ).on( 'change', function( e ) {
    9         $( '#code-filter' ).val( '' );
    10     } );
    11     $( '#code-filter' ).on( 'change', function( e ) {
    12         $( '#link-type-filter' ).val( '' );
    13     } );
    143
    154    // Nonces
     
    2413   
    2514    // Scan an individual link
    26     const scanLink = async ( link, postID, code, type, sourceID, method ) => {
     15    const scanLink = async ( link, linkID, code, type, sourceID, method ) => {
    2716        console.log( `Scanning link (${link})...` );
    2817
    2918        // Say it started
    30         var span = $( `#bln-verify-${postID}` );
     19        var span = $( `#bln-verify-${linkID}` );
    3120        span.addClass( 'scanning' ).html( `<em>Verifying</em>` );
    3221
     
    4029                nonce: nonceRescan,
    4130                link: link,
    42                 postID: postID,
     31                linkID: linkID,
    4332                code: code,
    4433                type: type,
     
    5847        for ( const linkSpan of linkSpans ) {
    5948            const link = linkSpan.dataset.link;
    60             const postID = linkSpan.dataset.postId;
     49            const linkID = linkSpan.dataset.linkId;
    6150            const code = linkSpan.dataset.code;
    6251            const type = linkSpan.dataset.type;
     
    6554
    6655            // Scan it
    67             const data = await scanLink( link, postID, code, type, sourceID, method );
     56            const data = await scanLink( link, linkID, code, type, sourceID, method );
    6857            console.log( data );
    6958
     
    9180                }
    9281               
    93                 $( `#post-${postID}` ).addClass( 'omitted' );
    94                 $( `#post-${postID} .bln-type` ).addClass( statusType ).text( statusType );
    95                 $( `#post-${postID} .bln_type code` ).html( 'Code: ' + statusCode );
    96                 $( `#post-${postID} .bln_type .message` ).text( statusText );
    97                 $( `#post-${postID} .title .row-actions` ).remove();
    98                 $( `#post-${postID} .bln_source .row-actions` ).remove();
     82                $( `#link-${linkID}` ).addClass( 'omitted' );
     83                $( `#link-${linkID} .bln-type` ).addClass( statusType ).text( statusType );
     84                $( `#link-${linkID} .bln_type code` ).html( 'Code: ' + statusCode );
     85                $( `#link-${linkID} .bln_type .message` ).text( statusText );
     86                $( `#link-${linkID} .link .row-actions` ).remove();
     87                $( `#link-${linkID} .source .row-actions` ).remove();
    9988
    10089                // Also reduce count in admin bar
    101                 reduceAdminBarCount();
     90                reduceCount();
    10291
    10392            } else if ( code != statusCode || type != statusType ) {
     
    10998                    text = `Link is still bad, but showing a different type. Old type was ${type}; new type is ${statusType}.`;
    11099                }
    111                 $( `#post-${postID} .bln-type` ).attr( 'class', `bln-type ${statusType}`).text( statusType );
     100                $( `#link-${linkID} .bln-type` ).attr( 'class', `bln-type ${statusType}`).text( statusType );
    112101                var codeLink = 'Code: ' + statusCode;
    113102                if ( statusCode != 0 && statusCode != 666 ) {
    114103                    codeLink = `<a href="https://http.dev/${statusCode}" target="_blank">Code: ${statusCode}</a>`;
    115104                }
    116                 $( `#post-${postID} .bln_type code` ).html( codeLink );
    117                 $( `#post-${postID} .bln_type message` ).text( statusText );
     105                $( `#link-${linkID} .bln_type code` ).html( codeLink );
     106                $( `#link-${linkID} .bln_type .message` ).text( statusText );
    118107            } else {
    119108                text = `Still showing ${statusType}.`;
     
    121110
    122111            // Update the page
    123             $( `#bln-verify-${postID}` ).removeClass( 'scanning' ).addClass( statusType ).html( text );
     112            $( `#bln-verify-${linkID}` ).removeClass( 'scanning' ).addClass( statusType ).html( text );
    124113        }
    125114
     
    137126     */
    138127
    139     $( document ).on( 'click', '.replace-link', function ( e ) {
     128    $( document ).on( 'click', '.replace-link a', function ( e ) {
    140129        e.preventDefault();
    141130   
    142         let linkElement = $( this ).closest( 'td' ).find( '.row-title' );
    143         let oldLink = linkElement.attr( 'href' );
    144         let postID = $( this ).data( 'post-id' );
    145         let sourceID = $( this ).data( 'source-id' );
     131        let linkElement = $( this ).closest( 'td' ).find( '.link-url' );
     132        let oldLink = $( this ).data( 'link' );
     133        let linkID = $( this ).closest( 'tr' ).data( 'link-id' );
     134        let sourceID = $( this ).closest( 'tr' ).find( '.source' ).data( 'source-id' );
    146135   
    147136        // Get the current link text
     
    159148        // Handle input field blur (when user clicks outside)
    160149        inputField.on( 'blur', function () {
    161             saveLink( $(this), oldLink, sourceID, postID );
     150            saveLink( $(this), oldLink, sourceID, linkID );
    162151        } );
    163152   
     
    171160   
    172161    // Function to save the link and replace the input field with a new link
    173     function saveLink( inputField, oldLink, sourceID, postID ) {
     162    function saveLink( inputField, oldLink, sourceID, linkID ) {
    174163        let newLink = inputField.val().trim();
    175164   
    176165        // If the new link is empty, revert to the original
    177166        if ( newLink === '' || newLink === oldLink || !sourceID ) {
    178             inputField.replaceWith( `<a class="row-title" href="${oldLink}" target="_blank">${oldLink}</a>` );
     167            inputField.replaceWith( `<a href="${oldLink}" class="link-url" target="_blank" rel="noopener">${oldLink}</a>` );
    179168            return;
    180169        }
    181         console.log( oldLink, newLink );
    182170   
    183171        // Create a new link element with the updated text
    184         let newLinkElement = $( `<a class="row-title" href="${newLink}" target="_blank">${newLink}</a>` );
     172        let newLinkElement = $( `<a href="${newLink}" class="link-url" target="_blank" rel="noopener">${newLink}</a>` );
    185173   
    186174        // Replace the input field with the new link
     
    195183                action: 'blnotifier_replace_link',
    196184                nonce: nonceReplace,
    197                 resultID: postID,
    198185                oldLink: oldLink,
    199186                newLink: newLink,
     
    204191                    console.log( 'Link updated successfully.' );
    205192
     193                    // Replace the old data-link in the Replace Link attribute
     194                    $( `#link-${linkID} .link .row-actions .replace-link a` ).data( 'link', newLink ).attr( 'data-link', newLink );
     195
    206196                    // Replace the source blink
    207                     let viewPageLink = $( `tr#post-${postID} .column-bln_source .row-actions .view a` );
     197                    let viewPageLink = $( `tr#link-${linkID} .source .row-actions .view a` );
    208198                    let currentHref = viewPageLink.attr( 'href' );
    209199                    let newBlinkUrl = encodeURIComponent( newLink );
     
    212202
    213203                    // Update the type
    214                     $( `#post-${postID} .bln-type` ).addClass( 'fixed' ).text( 'Replaced' );
    215                     $( `#post-${postID} .bln_type code` ).remove();
    216                     $( `#post-${postID} .bln_type .message` ).html( `The old link has been replaced. Result will be removed.<br>Old link: ${oldLink}` );
     204                    $( `#link-${linkID} .bln-type` ).addClass( 'fixed' ).text( 'Replaced' );
     205                    $( `#link-${linkID} .bln_type code` ).remove();
     206                    $( `#link-${linkID} .bln_type .message` ).html( `The old link has been replaced. Result will be removed.<br>Old link: ${oldLink}` );
    217207
    218208                    // Remove omit link action
    219                     $( `#post-${postID} .column-title .row-actions .clear` ).remove();
    220                     $( `#post-${postID} .column-title .row-actions .omit` ).remove();
     209                    $( `#link-${linkID} .link .row-actions .clear-result` ).remove();
     210                    $( `#link-${linkID} .link .row-actions .omit-link` ).remove();
    221211
    222212                    // Update the Verify column
    223                     $( `#bln-verify-${postID}` ).text( `N/A` );
    224 
    225                     let rowActions = $( `#post-${postID} .column-title .row-actions` );
     213                    $( `#bln-verify-${linkID}` ).text( `N/A` );
     214
     215                    let rowActions = $( `#link-${linkID} .link .row-actions` );
    226216                    if ( rowActions.children().length === 1 ) {
    227217                        rowActions.html( rowActions.html().replace(' | ', '' ) );
     
    229219
    230220                    // Reduce admin bar count
    231                     reduceAdminBarCount();
     221                    reduceCount();
    232222                   
    233223                } else {
     
    246236     */
    247237
    248     $( document ).on( 'click', '.clear-result', function ( e ) {
     238    $( document ).on( 'click', '.clear-result a', function ( e ) {
    249239        e.preventDefault();
    250240
    251241        let button = $( this );
    252         let postID = button.data( 'post-id' );
     242        let link = button.data( 'link' );
    253243
    254244        $.ajax( {
     
    259249                action: 'blnotifier_delete_result',
    260250                nonce: nonceDelete,
    261                 postID: postID
     251                link: link
    262252            },
    263253            success: function ( response ) {
     
    265255                    button.closest( 'tr' ).fadeOut( 'fast', function () {
    266256                        $( this ).remove();
     257                        reduceCount();
    267258                    } );
    268 
    269                     // Reduce admin bar count
    270                     reduceAdminBarCount();
    271259
    272260                } else {
     
    290278        let button = $( this );
    291279        let sourceID = button.data( 'source-id' );
    292         console.log( sourceID );
    293280        let postTitle = button.data( 'source-title' );
    294281
     
    311298                    $( 'tr' ).each( function () {
    312299                        let row = $( this );
    313                         let rowSourceID = row.find( '.row-actions[data-source-id]' ).attr( 'data-source-id' );
     300                        let rowSourceID = row.find( '.source[data-source-id]' ).attr( 'data-source-id' );
    314301   
    315302                        if ( rowSourceID == sourceID ) {
    316303                            row.fadeOut( 'fast', function () {
    317304                                $( this ).remove();
     305                                reduceCount();
    318306                            } );
    319 
    320                             // Reduce admin bar count
    321                             reduceAdminBarCount();
    322307                        }
    323308                    } );
     
    337322     */
    338323
    339     function reduceAdminBarCount() {
     324    function reduceCount() {
     325        // Count broken links currently on the page
     326        var countBroken = $( 'tr .bln-type.broken' ).length;
     327        var countAll = $( 'tr .bln-type' ).length;
     328
     329        // Update admin bar
    340330        var adminBarEl = $( '#wp-admin-bar-blnotifier-notify' );
    341331        if ( adminBarEl.length ) {
    342             var adminBarCountEl = adminBarEl.find( '.awaiting-mod' );
    343             var adminBarCount = parseInt( adminBarCountEl.text(), 10 );
    344    
    345             if ( !isNaN( adminBarCount ) && adminBarCount > 0 ) {
    346                 adminBarCountEl.text( adminBarCount - 1 );
    347             }
    348         }
    349     }
    350    
     332            adminBarEl.find( '.blnotifier-count-indicator' ).text( countBroken );
     333        }
     334
     335        // Update admin menu
     336        var adminMenuEl = $( 'li.toplevel_page_broken-link-notifier' );
     337        if ( adminMenuEl.length ) {
     338            adminMenuEl.find( '.awaiting-mod' ).text( countBroken );
     339        }
     340
     341        // Update page total counter
     342        var pageCountEl = $( '#bln-total-broken-links' );
     343        if ( pageCountEl.length ) {
     344            pageCountEl.text( countAll );
     345        }
     346    }
     347
     348
     349    /**
     350     * TRASH SELECTED BUTTON
     351     */
     352    function updateDeleteButton() {
     353        const checkedCount = $( '.bln-row-checkbox:checked' ).length;
     354        $( '#bln-delete-selected' ).prop( 'disabled', checkedCount === 0 );
     355    }
     356
     357    $( document ).on( 'change', '.bln-row-checkbox', function() {
     358        updateDeleteButton();
     359    } );
     360
     361    $( '#cb-select-all-1' ).on( 'change', function() {
     362        const checked = $( this ).prop( 'checked' );
     363        $( '.bln-row-checkbox' ).prop( 'checked', checked );
     364        updateDeleteButton();
     365    } );
    351366} )
  • broken-link-notifier/trunk/includes/menu.php

    r3471187 r3475895  
    5656        // The menu items
    5757        $this->menu_items = [
    58             'results'     => [ __( 'Results', 'broken-link-notifier' ), 'edit.php?post_type=blnotifier-results' ],
    59             'omit-links'  => [ __( 'Omitted Links', 'broken-link-notifier' ), 'edit-tags.php?taxonomy=omit-links&post_type=blnotifier-results' ],
    60             'omit-pages'  => [ __( 'Omitted Pages', 'broken-link-notifier' ), 'edit-tags.php?taxonomy=omit-pages&post_type=blnotifier-results' ],
     58            'results'     => [ __( 'Results', 'broken-link-notifier' ) ],
     59            'omit-links'  => [ __( 'Omitted Links', 'broken-link-notifier' ), 'edit-tags.php?taxonomy=omit-links' ],
     60            'omit-pages'  => [ __( 'Omitted Pages', 'broken-link-notifier' ), 'edit-tags.php?taxonomy=omit-pages' ],
    6161            'scan-single' => [ __( 'Page Scan', 'broken-link-notifier' ) ],
    6262            'scan-multi'  => [ __( 'Multi-Scan', 'broken-link-notifier' ) ],
     
    206206        // Taxonomies first
    207207        } elseif ( $current_screen->id == 'edit-omit-links' ) {
    208             $submenu_file = 'edit-tags.php?taxonomy=omit-links&post_type=blnotifier-results';
     208            $submenu_file = 'edit-tags.php?taxonomy=omit-links';
    209209            $parent_file = $this->get_plugin_page_short_path( null );
    210210        } elseif ( $current_screen->id == 'edit-omit-pages' ) {
    211             $submenu_file = 'edit-tags.php?taxonomy=omit-pages&post_type=blnotifier-results';
     211            $submenu_file = 'edit-tags.php?taxonomy=omit-pages';
    212212            $parent_file = $this->get_plugin_page_short_path( null );
    213        
    214         // Post Type
    215         } elseif ( $current_screen->post_type == 'blnotifier-results' ) {
    216             $submenu_file = 'edit.php?post_type=blnotifier-results';
    217             $parent_file = $this->get_plugin_page_short_path();
    218213        }
    219214
     
    582577            ]
    583578        );   
     579
     580        // Uninstall database cleanup
     581        $uninstall_cleanup_option_name = 'blnotifier_uninstall_cleanup';
     582        register_setting( $this->page_slug, $uninstall_cleanup_option_name, [ $this, 'sanitize_checkbox' ] );
     583        add_settings_field(
     584            $uninstall_cleanup_option_name,
     585            'Remove Data on Uninstall',
     586            [ $this, 'field_checkbox' ],
     587            $this->page_slug,
     588            'general',
     589            [
     590                'class'    => $uninstall_cleanup_option_name,
     591                'name'     => $uninstall_cleanup_option_name,
     592                'default'  => false,
     593                'comments' => 'Enable this option to automatically remove the database table that the links are stored in and all options when the plugin is uninstalled.'
     594            ]
     595        );
    584596    } // End settings_fields()
    585597
     
    981993     */
    982994    public function get_plugin_page( $tab = 'settings' ) {
    983         if ( $tab == 'results' ) {
    984             return admin_url( 'edit.php?post_type=blnotifier-results' );
    985         } elseif ( $tab == 'omit-links' || $tab == 'omit-pages' ) {
    986             return admin_url( 'edit-tags.php?taxonomy='.$tab.'&post_type=blnotifier-results' );
     995        if ( $tab == 'omit-links' || $tab == 'omit-pages' ) {
     996            return admin_url( 'edit-tags.php?taxonomy='.$tab );
    987997        } else {
    988998            return admin_url( 'admin.php?page='.BLNOTIFIER_TEXTDOMAIN ).'&tab='.sanitize_key( $tab );
  • broken-link-notifier/trunk/includes/omits.php

    r3471187 r3475895  
    123123
    124124        // Register it as a new taxonomy
    125         register_taxonomy( $taxonomy, 'blnotifier-results', [
     125        register_taxonomy( $taxonomy, [], [
    126126            // 'hierarchical'       => false,
    127127            'labels'             => $labels,
     
    292292            // Also delete it from results
    293293            if ( $page && $page == 'scan-results' ) {
    294                 if ( $post_id = post_exists( $link ) ) {
    295                     if ( get_post_type( $post_id ) == (new BLNOTIFIER_RESULTS())->post_type ) {
    296                         wp_delete_post( $post_id, true );
    297                     }
    298                 }
     294                (new BLNOTIFIER_RESULTS)->remove( $link );
    299295            }
    300296            return true;
     
    419415        $options_page = 'toplevel_page_'.BLNOTIFIER_TEXTDOMAIN;
    420416        $tab = (new BLNOTIFIER_HELPERS)->get_tab();
    421         $post_type = get_post_type();
    422         if ( ( $screen == $options_page && $tab == 'scan-single' ) || ( $screen == 'edit.php' && ( isset( $_REQUEST[ '_wpnonce' ] ) && wp_verify_nonce( sanitize_text_field( wp_unslash ( $_REQUEST[ '_wpnonce' ] ) ), 'blnotifier_blinks' ) && isset( $_GET[ 'blinks' ] ) && sanitize_key( $_GET[ 'blinks' ] ) == 'true' )  || $post_type == 'blnotifier-results' ) ) {
    423             if ( !$tab && $post_type == 'blnotifier-results' ) {
     417
     418        if (
     419            ( $screen == $options_page && $tab == 'scan-single' ) ||
     420            ( $screen == $options_page && $tab == 'results' ) ||
     421            ( $screen == 'edit.php' &&
     422                (
     423                    isset( $_REQUEST[ '_wpnonce' ] ) &&
     424                    wp_verify_nonce( sanitize_text_field( wp_unslash ( $_REQUEST[ '_wpnonce' ] ) ), 'blnotifier_blinks' ) &&
     425                    isset( $_GET[ 'blinks' ] ) &&
     426                    sanitize_key( $_GET[ 'blinks' ] ) == 'true'
     427                )
     428            )
     429        ) {
     430       
     431        if ( $tab == 'results' ) {
    424432                $tab = 'scan-results';
    425433            } elseif ( !$tab ) {
  • broken-link-notifier/trunk/includes/results.php

    r3471187 r3475895  
    2121
    2222/**
    23  * Main plugin class.
     23 * Results class.
    2424 */
    2525class BLNOTIFIER_RESULTS {
     
    3030     * @var string
    3131     */
    32     public $post_type = 'blnotifier-results';
     32    public $table_name = 'blnotifier_results';
    3333
    3434
     
    6262    public function init() {
    6363
    64         // Register the post type
    65         $this->register_post_type();
     64        // Maybe create the database table
     65        $this->maybe_create_db();
    6666
    6767        // Add the header to the top of the admin list page
    6868        add_action( 'load-edit.php', [ $this, 'add_header' ] );
    6969        add_action( 'load-edit-tags.php', [ $this, 'add_header' ] );
    70 
    71         // Add instructions to top of page
    72         add_action( 'admin_notices', [ $this, 'description_notice' ] );
    73 
    74         // Redirect
    75         add_filter( 'get_edit_post_link', [ $this, 'redirect' ], 10, 3 );
    76 
    77         // Remove Edit from Bulk Actions
    78         add_filter( 'bulk_actions-edit-'.$this->post_type, [ $this, 'remove_from_bulk_actions' ] );
    79 
    80         // Remove post states
    81         add_filter( 'display_post_states', [ $this, 'remove_post_states' ], 999, 2 );
    82 
    83         // Remove Edit and Quick Edit links, and add ignore link
    84         add_action( 'post_row_actions', [ $this, 'row_actions' ], 10, 2 );
    85 
    86         // Skip trash and auto delete
    87         add_action( 'trashed_post', [ $this, 'skip_trash' ] );
    88 
    89         // Add a type filter
    90         add_action( 'restrict_manage_posts', [ $this, 'admin_filters' ], 10, 2 );
    91         add_action( 'pre_get_posts', [ $this, 'admin_filters_query' ] );
    92 
    93         // Add admin columns
    94         add_filter( 'manage_'.$this->post_type.'_posts_columns', [ $this, 'admin_columns' ] );
    95         add_action( 'manage_'.$this->post_type.'_posts_custom_column', [ $this, 'admin_column_content' ], 10, 2 );
    96 
    97         // Make admin columns sortable
    98         add_filter( 'manage_edit-'.$this->post_type.'_sortable_columns', [ $this, 'sort_columns' ] );
    99         add_action( 'pre_get_posts', [ $this, 'sort_columns_query' ] );
    10070
    10171        // Add notifications to admin bar
     
    11888
    11989    } // End init()
    120 
    121    
    122     /**
    123      * Register the post type
    124      */
    125     public function register_post_type() {
    126         // Set the labels
    127         $labels = [
    128             'name'                  => _x( 'Links', 'Post Type General Name', 'broken-link-notifier' ),
    129             'singular_name'         => _x( 'Link', 'Post Type Singular Name', 'broken-link-notifier' ),
    130             'menu_name'             => __( 'Links', 'broken-link-notifier' ),
    131             'name_admin_bar'        => __( 'Links', 'broken-link-notifier' ),
    132             'search_items'          => __( 'Search links', 'broken-link-notifier' ),
    133             'not_found'             => __( 'Not found', 'broken-link-notifier' ),
    134             'not_found_in_trash'    => __( 'Not found in Trash', 'broken-link-notifier' ),
    135             'filter_items_list'     => __( 'Filter link list', 'broken-link-notifier' ),
    136         ];
    137    
    138         // Set the CPT args
    139         $args = [
    140             'label'                 => __( 'Links', 'broken-link-notifier' ),
    141             'description'           => __( 'Links', 'broken-link-notifier' ),
    142             'labels'                => $labels,
    143             'supports'              => [],
    144             'taxonomies'            => [],
    145             'public'                => false,
    146             'show_ui'               => true,
    147             'show_in_menu'          => false,
    148             'show_in_admin_bar'     => false,
    149             'show_in_nav_menus'     => false,
    150             'can_export'            => true,
    151             'has_archive'           => false,
    152             'exclude_from_search'   => true,
    153             'publicly_queryable'    => false,
    154             'query_var'             => $this->post_type,
    155             'capability_type'       => 'post',
    156             'capabilities'          => [
    157                 'create_posts'      => 'do_not_allow',
    158             ],
    159             'map_meta_cap'          => true,
    160             'show_in_rest'          => true,
    161         ];
    162    
    163         // Register the CPT
    164         register_post_type( $this->post_type, $args );
    165     } // End register_post_type()
    16690
    16791
     
    235159
    236160    /**
    237      * Add a description notice
    238      *
    239      * @return void
    240      */
    241     public function description_notice() {
    242         global $current_screen;
    243         if ( 'edit-'.$this->post_type === $current_screen->id ) {
    244             echo '<div class="notice notice-info" >
    245                 <p>' . esc_html__( 'This page shows the results of your scans. It automatically rechecks the links themselves to see if they are still broken, but it does not remove the links from the pages or rescan the pages to see if broken links have been fixed. After fixing a broken link, you will need to clear the result below. Then when you rescan the page it should not show up here again. Note that the plugin will still find broken links if you simply hide them on the page.', 'broken-link-notifier' ) . '</p>
    246             </div>';
    247         }
    248     } // End description_notice()
    249 
    250 
    251     /**
    252      * Redirect the edit link
    253      *
    254      * @param string $url
    255      * @param int $post_id
    256      * @param [type] $context
    257      * @return string
    258      */
    259     public function redirect( $url, $post_id, $context ) {
    260         if ( get_post_type( $post_id ) == $this->post_type ) {
    261             return sanitize_text_field( get_the_title( $post_id ) );
    262         } else {
    263             return $url;
    264         }
    265     } // End redirect()
    266  
    267 
    268     /**
    269      * Remove Edit from Bulk Actions
    270      *
    271      * @param array $actions
    272      * @return array
    273      */
    274     public function remove_from_bulk_actions( $actions ) {
    275         unset( $actions[ 'edit' ] );
    276         return $actions;
    277     } // End remove_from_bulk_actions()
    278 
    279 
    280     /**
    281      * Remove states
    282      *
    283      * @param array $states
    284      * @param WP_Post $post
    285      * @return void
    286      */
    287     public function remove_post_states( $states, $post ) {
    288         if ( get_post_type( $post ) == $this->post_type ) {
    289             return false;
    290         }
    291         return $states;
    292     } // End remove_post_states()
    293 
    294 
    295     /**
    296      * Action links
    297      *
    298      * @param array $actions
    299      * @param object $post
    300      * @return array
    301      */
    302     public function row_actions( $actions, $post ) {
    303         if ( $this->post_type == $post->post_type ) {
    304             $actions = [];
    305 
    306             $actions[ 'clear' ] = '<a class="clear-result" href="#" data-post-id="' . $post->ID . '">' . __( 'Clear Result', 'broken-link-notifier' ) . '</a></span>';
    307 
    308             $permalink = get_the_title( $post );
    309             if ( !(new BLNOTIFIER_OMITS)->is_omitted( $permalink, 'links' ) ) {
    310                 $actions[ 'omit' ] = '<a class="omit-link" href="#" data-link="' . $permalink . '" data-post-id="' . $post->ID . '">' . __( 'Omit Link', 'broken-link-notifier' ) . '</a>';
    311             }
    312 
    313             $source_url = get_post_meta( $post->ID, 'source', true );
    314             if ( $source_url ) {
    315                 $source_url = filter_var( $source_url, FILTER_SANITIZE_URL );
    316                
    317                 if ( $source_id = url_to_postid( $source_url ) ) {
    318                     $actions[ 'replace' ] = '<a class="replace-link" href="#" data-link="' . $permalink . '" data-post-id="' . $post->ID . '" data-source-id="' . $source_id . '">' . __( 'Replace Link', 'broken-link-notifier' ) . '</a></span>';
    319                 }
    320             }
    321         }
    322         return $actions;
    323     } // End row_actions()
    324 
    325 
    326     /**
    327      * Skip trash and auto delete
    328      *
    329      * @param int $post_id
    330      * @return void
    331      */
    332     public function skip_trash( $post_id ) {
    333         if ( get_post_type( $post_id ) == $this->post_type ) {
    334             wp_delete_post( $post_id, true );
    335         }
    336     } // End skip_trash()
    337 
    338 
    339     /**
    340      * Type filter
    341      *
    342      * @return void
    343      */
    344     public function admin_filters( $post_type, $which ) {
    345         $screen = get_current_screen();
    346         if ( $screen && $screen->id == 'edit-'.$this->post_type ) {
    347 
    348             // Link type
    349             if ( isset( $_REQUEST[ 'link-type' ] ) ) {  // phpcs:ignore
    350                 $s = sanitize_key( $_REQUEST[ 'link-type' ] ); // phpcs:ignore
    351             } else {
    352                 $s = '';
    353             }
    354             $HELPERS = new BLNOTIFIER_HELPERS;
    355             echo '<select id="link-type-filter" name="link-type">
    356                 <option value=""'.esc_html( $HELPERS->is_selected( $s, '' ) ).'>All Link Types</option>
    357                 <option value="broken"'.esc_html( $HELPERS->is_selected( $s, 'broken' ) ).'>Broken Links</option>
    358                 <option value="warning"'.esc_html( $HELPERS->is_selected( $s, 'warning' ) ).'>Warning Links</option>
    359             </select>';
    360 
    361             // Status code
    362             $bad_codes = $HELPERS->get_bad_status_codes();
    363             $warning_codes = $HELPERS->get_warning_status_codes();
    364             $all_codes = array_unique( array_merge( $bad_codes, $warning_codes ) );
    365             sort( $all_codes );
    366 
    367             if ( isset( $_GET[ 'code' ] ) && sanitize_text_field( $_GET[ 'code' ] ) != '' ) { // phpcs:ignore
    368                 $s = absint( $_GET[ 'code' ] ); // phpcs:ignore
    369             } else {
    370                 $s = '';
    371             }
    372             $HELPERS = new BLNOTIFIER_HELPERS;
    373             echo '<select id="code-filter" name="code">
    374                 <option value=""'.esc_html( $HELPERS->is_selected( $s, '' ) ).'>All Status Codes</option>';
    375 
    376                 foreach ( $all_codes as $code ) {
    377                     echo '<option value="'.esc_attr( $code ).'"'.esc_html( $HELPERS->is_selected( $s, $code ) ).'>'.esc_attr( $code ).'</option>';
    378                 }
    379 
    380             echo '</select>';
    381         }
    382     } // End admin_filters()
    383 
    384 
    385     /**
    386      * The type filter query
    387      *
    388      * @param object $query
    389      * @return void
    390      */
    391     public function admin_filters_query( $query ) {
    392         $post_type = $query->get( 'post_type' );
    393         if ( $post_type == $this->post_type ) {
    394 
    395             // Link type
    396             if ( isset( $_REQUEST[ 'link-type' ] ) && sanitize_text_field( $_REQUEST[ 'link-type' ] ) != '' ) { // phpcs:ignore
    397                 $type = sanitize_key( $_REQUEST[ 'link-type' ] ); // phpcs:ignore
    398                 if ( $type != '' ) {
    399                     $meta_query[] = [
    400                         'key'     => 'type',
    401                         'value'   => $type,
    402                     ];
    403                     $query->set( 'meta_query', $meta_query );
    404                 }
    405 
    406             // Status code
    407             } elseif ( isset( $_REQUEST[ 'code' ] ) && sanitize_text_field( $_REQUEST[ 'code' ] ) != '' ) { // phpcs:ignore
    408                 $code = absint( $_REQUEST[ 'code' ] ); // phpcs:ignore
    409                 if ( $code != '' ) {
    410                     $meta_query[] = [
    411                         'key'     => 'code',
    412                         'value'   => $code,
    413                     ];
    414                     $query->set( 'meta_query', $meta_query );
    415                 }
    416             }
    417         }
    418     } // End admin_filters_query()
    419 
    420    
    421     /**
    422      * Admin columns
    423      *
    424      * @param array $columns
    425      * @return array
    426      */
    427     public function admin_columns( $columns ) {
    428         return [
    429             'cb'            => '<input type="checkbox"/>',
    430             'bln_type'      => __( 'Type', 'broken-link-notifier' ),
    431             'title'         => __( 'Link', 'broken-link-notifier' ),
    432             'bln_source'    => __( 'Source', 'broken-link-notifier' ),
    433             'bln_source_pt' => __( 'Source Post Type', 'broken-link-notifier' ),
    434             'bln_date'      => __( 'Date', 'broken-link-notifier' ),
    435             'bln_author'    => __( 'User', 'broken-link-notifier' ),
    436             'bln_method'    => __( 'Method', 'broken-link-notifier' ),
    437             'bln_verify'    => __( 'Verify', 'broken-link-notifier' ),
    438         ];
    439     } // End admin_columns()
    440 
    441 
    442     /**
    443      * Admin column content
    444      *
    445      * @param string $column
    446      * @param int $post_id
    447      * @return void
    448      */
    449     public function admin_column_content( $column, $post_id ) {
    450         // Type
    451         if ( 'bln_type' === $column ) {
    452             $post = get_post( $post_id );
    453             if ( $post->type == 'broken' ) {
    454                 echo '<div class="bln-type broken">' . esc_html( __( 'Broken', 'broken-link-notifier' ) ) . '</div>';
    455             } elseif ( $post->type == 'warning' ) {
    456                 echo '<div class="bln-type warning">' . esc_html( __( 'Warning', 'broken-link-notifier' ) ) . '</div>';
    457             } elseif ( $post->type == 'good' ) {
    458                 echo '<div class="bln-type good">' . esc_html( __( 'Good', 'broken-link-notifier' ) ) . '</div>';
    459             }
    460             $code = $post->code;
    461             if ( $code != 0 && $code != 666 ) {
    462                 $code = '<a href="https://http.dev/'.$code.'" target="_blank">'.$code.'</a>';
    463             }
    464             if ( $code == 666 ) {
    465                 $incl_title = ' title="' . __( 'A status code of 666 is a code we use to force invalid URL code 0 to be a broken link. It is not an official status code.', 'broken-link-notifier' ) . '"';
    466             } elseif ( $code == 0 ) {
    467                 $incl_title = ' title="' . __( 'A status code of 0 means there was no response and it can occur for various reasons, like request time outs. It almost always means something is randomly interfering with the user\'s connection, like a proxy server / firewall / load balancer / laggy connection / network congestion, etc.', 'broken-link-notifier' ) . '"';
    468             } else {
    469                 $incl_title = '';
    470             }
    471             echo '<code'.wp_kses_post( $incl_title ).'>Code: '.wp_kses_post( $code ).'</code> <span class="message">'.esc_html( $post->post_content ).'</span>';
    472         }
    473 
    474         // Source
    475         if ( 'bln_source' === $column ) {
    476             $source_url = get_post_meta( $post_id, 'source', true );
    477             if ( $source_url ) {
    478                 $source_url = filter_var( $source_url, FILTER_SANITIZE_URL );
    479                 $source_url = remove_query_arg( (new BLNOTIFIER_HELPERS)->get_qs_to_remove_from_source(), $source_url );
    480                 $actions = [
    481                     '<span class="view"><a href="'.add_query_arg( 'blink', urlencode( strtok( get_the_title( $post_id ), '?' ) ), $source_url ).'" target="_blank">' . __( 'View Page', 'broken-link-notifier' ) . '</a></span>'
    482                 ];
    483                 if ( $source_id = url_to_postid( $source_url ) ) {
    484                     if ( !(new BLNOTIFIER_OMITS)->is_omitted( $source_url, 'pages' ) ) {
    485                         $actions[] = '<span class="omit"><a class="omit-page" href="#" data-link="'.$source_url.'">' . __( 'Omit Page', 'broken-link-notifier' ) . '</a></span>';
    486                     }
    487                     $scan_nonce = wp_create_nonce( 'blnotifier_scan_single' );
    488                     $actions[] = '<span class="scan"><a class="scan-page" href="'.(new BLNOTIFIER_MENU)->get_plugin_page( 'scan-single' ).'&scan='.$source_url.'&_wpnonce='.$scan_nonce.'" target="_blank">Scan Page</a></span>';
    489                     $actions[] = '<span class="edit"><a href="'.admin_url( 'post.php' ).'?post='.$source_id.'&action=edit">' . __( 'Edit Page', 'broken-link-notifier' ) . '</a></span>';
    490                     if ( is_plugin_active( 'cornerstone/cornerstone.php' ) ) {
    491                         $actions[] = '<span class="edit-in-cornerstone"><a href="'.home_url( '/cornerstone/edit/'.$source_id ).'">' . __( 'Edit in Cornerstone', 'broken-link-notifier' ) . '</a></span>';
    492                     }
    493 
    494                     $title = get_the_title( $source_id );
    495                     $source_url = '<strong><a class="source-url" href="'.$source_url.'">'.$title.'</a></strong>';
    496 
    497                     if ( get_option( 'blnotifier_enable_delete_source' ) ) {
    498                         $actions[] = '<span class="delete"><a class="delete-source" href="#" data-source-title="' . $title . '" data-source-id="' . $source_id . '">' . __( 'Trash Page', 'broken-link-notifier' ) . '</a></span>';
    499                     }
    500                 }
    501 
    502                 echo wp_kses_post( $source_url ).'<div class="row-actions" data-source-id="' . esc_attr( $source_id ) . '">'.wp_kses_post( implode( ' | ', $actions ) ).'</div>';
    503             } else {
    504                 echo esc_html__( 'No source found...', 'broken-link-notifier' );
    505             }
    506         }
    507 
    508         // Source URL
    509         if ( 'bln_source_pt' === $column ) {
    510             $url = get_post_meta( $post_id, 'source', true );
    511             if ( $url ) {
    512                 $url = filter_var( $url, FILTER_SANITIZE_URL );
    513                 if ( $source_id = url_to_postid( $url ) ) {
    514                     $post_type = get_post_type( $source_id );
    515                     $post_type_name = (new BLNOTIFIER_HELPERS)->get_post_type_name( $post_type, true );
    516                 } else {
    517                     $post_type_name = '--';
    518                 }
    519                 echo esc_html( $post_type_name );
    520             }
    521         }
    522 
    523         // Date
    524         if ( 'bln_date' === $column ) {
    525             $date = get_the_date( 'F j, Y g:i A', $post_id );
    526             $location = get_post_meta( $post_id, 'location', true );
    527             echo 'Discovered in '.esc_html( ucwords( $location ) ).'<br>'.esc_html( $date );
    528         }
    529 
    530         // Author
    531         if ( 'bln_author' === $column ) {
    532             $post = get_post( $post_id );
    533             if ( isset( $post->guest ) && $post->guest ) {
    534                 $display_name = 'Guest';
    535             } elseif ( $author = $post->post_author ) {
    536                 $user = get_user_by( 'ID', $author );
    537                 $display_name = $user->display_name;
    538             } else {
    539                 $display_name = 'Guest';
    540             }
    541             echo esc_html( $display_name );
    542         }
    543 
    544         // Method
    545         if ( 'bln_method' === $column ) {
    546             $post = get_post( $post_id );
    547             if ( isset( $post->method ) && $post->method ) {
    548 
    549                 switch ( sanitize_key( $post->method ) ) {
    550                     case 'visit':
    551                         $method = __( 'Front-End Visit', 'broken-link-notifier' );
    552                         break;
    553                     case 'multi':
    554                         $method = __( 'Multi-Scan', 'broken-link-notifier' );
    555                         break;
    556                     case 'single':
    557                         $method = __( 'Page Scan', 'broken-link-notifier' );
    558                         break;
    559                     default:
    560                         $method = __( 'Unknown', 'broken-link-notifier' );
    561                 }
    562             } else {
    563                 $method = __( 'Unknown', 'broken-link-notifier' );
    564             }
    565             echo esc_html( $method );
    566         }
    567 
    568         // Verify
    569         if ( 'bln_verify' === $column ) {
    570 
    571             if ( !(new BLNOTIFIER_HELPERS ())->is_results_verification_paused() ) {
    572                 $link = get_the_title( $post_id );
    573                 $post = get_post( $post_id );
    574                 $code = absint( $post->code );
    575                 $type = sanitize_text_field( $post->type );
    576                 $source_url = filter_var( $post->source, FILTER_SANITIZE_URL );
    577                 $source_url = remove_query_arg( (new BLNOTIFIER_HELPERS)->get_qs_to_remove_from_source(), $source_url );
    578                 $source_id = url_to_postid( $source_url );
    579                 $method = $post->method ? sanitize_key( $post->method ) : 'unknown';
    580 
    581                 echo '<span id="bln-verify-'.esc_attr( $post_id ).'" class="bln-verify" data-type="' . esc_attr( $type ) . '" data-post-id="'.esc_attr( $post_id ).'" data-link="'.esc_html( $link ).'" data-code="'.esc_attr( $code ).'" data-source-id="'.esc_attr( $source_id ).'" data-method="'.esc_attr( $method ).'">' . esc_html__( 'Pending', 'broken-link-notifier' ) . '</span>';
    582 
    583             } else {
    584                 echo esc_html__( 'Auto-verification is paused in settings.', 'broken-link-notifier' );
    585             }
    586 
    587         }
    588     } // End admin_column_content()
    589    
    590 
    591     /**
    592      * Make admin columns sortable
    593      *
    594      * @param array $columns
    595      * @return array
    596      */
    597     public function sort_columns( $columns ) {
    598         $columns[ 'bln_type' ]      = 'bln_type';
    599         $columns[ 'bln_source' ]    = 'bln_source';
    600         $columns[ 'bln_source_pt' ] = 'bln_source_pt';
    601         $columns[ 'bln_date' ]      = 'bln_date';
    602         return $columns;
    603     } // End sort_columns()
    604 
    605 
    606     /**
    607      * Sort the order column properly
    608      *
    609      * @param object $query
    610      * @return void
    611      */
    612     public function sort_columns_query( $query ) {
    613         global $current_screen;
    614         if ( is_admin() && isset( $current_screen ) && $current_screen->id === 'edit-'.$this->post_type ) {
    615             $orderby = $query->get( 'orderby' );
    616             if ( 'bln_type' == $orderby ) {
    617                 $query->set( 'meta_key', 'type' );
    618                 $query->set( 'orderby', 'meta_value' );
    619             } elseif ( 'bln_source' == $orderby ) {
    620                 $query->set( 'meta_key', 'bln_source' );
    621                 $query->set( 'orderby', 'meta_value' );
    622             } elseif ( 'bln_date' == $orderby ) {
    623                 // $query->set( 'meta_key', 'date' );
    624                 $query->set( 'orderby', 'date' );
    625             }
    626         }
    627     } // End sort_columns_query()
    628 
    629 
    630     /**
    631161     * Add an online user count to the admin bar
    632162     *
     
    681211
    682212    /**
     213     * Create the database table if it doesn't exist.
     214     *
     215     * @return void
     216     */
     217    public function maybe_create_db() {
     218        global $wpdb;
     219
     220        $table_name = $wpdb->prefix . $this->table_name;
     221       
     222        if ( $wpdb->get_var( "SHOW TABLES LIKE '{$table_name}'" ) !== $table_name ) {
     223            require_once ABSPATH . 'wp-admin/includes/upgrade.php';
     224            $charset_collate = $wpdb->get_charset_collate();
     225
     226            $sql = "CREATE TABLE $table_name (
     227                id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
     228                link varchar(2048) NOT NULL,
     229                link_hash char(32) NOT NULL,
     230                text varchar(255) NOT NULL,
     231                type varchar(20) NOT NULL,
     232                code smallint(5) unsigned NOT NULL,
     233                source varchar(2048) NOT NULL,
     234                location varchar(50) NOT NULL,
     235                method varchar(20) NOT NULL,
     236                guest tinyint(1) NOT NULL DEFAULT 0,
     237                author_id bigint(20) unsigned NOT NULL DEFAULT 0,
     238                created_at datetime NOT NULL,
     239                PRIMARY KEY (id),
     240                UNIQUE KEY link_hash (link_hash),
     241                KEY code (code),
     242                KEY created_at (created_at)
     243            ) $charset_collate;";
     244
     245            dbDelta( $sql );
     246
     247            // Migrate old posts if they exist
     248            $old_posts = get_posts( [
     249                'post_type'      => 'blnotifier-results',
     250                'posts_per_page' => -1,
     251                'post_status'    => 'publish',
     252                'fields'         => 'ids',
     253            ] );
     254
     255            if ( ! empty( $old_posts ) ) {
     256                foreach ( $old_posts as $post_id ) {
     257                    $code     = absint( get_post_meta( $post_id, 'code', true ) );
     258                    $location = get_post_meta( $post_id, 'location', true );
     259                    $method   = get_post_meta( $post_id, 'method', true );
     260                    $source   = get_post_meta( $post_id, 'source', true );
     261                    $type     = get_post_meta( $post_id, 'type', true );
     262
     263                    $post    = get_post( $post_id );
     264                    $link    = $post ? $post->post_title : '';
     265                    $text    = $post ? $post->post_content : '';
     266                    $author  = $post ? $post->post_author : 0;
     267                    $created = $post ? $post->post_date : current_time( 'mysql' );
     268
     269                    $this->add( [
     270                        'link'   => $link,
     271                        'text'   => $text,
     272                        'type'   => $type,
     273                        'code'   => $code,
     274                        'source' => $source,
     275                        'location' => $location,
     276                        'method' => $method,
     277                        'author' => $author,
     278                        'created_at' => $created,
     279                    ] );
     280
     281                    // Delete the old post
     282                    wp_delete_post( $post_id, true );
     283                }
     284            }
     285        }
     286    } // End maybe_create_db()
     287
     288
     289    /**
    683290     * Check if the link has already been added
    684291     *
     
    687294     */
    688295    public function already_added( $link ) {
    689         return post_exists( sanitize_text_field( $link ), '', '', $this->post_type );
     296        global $wpdb;
     297
     298        $table_name = $wpdb->prefix . $this->table_name;
     299        $link_clean = sanitize_text_field( $link );
     300        $link_hash = md5( strtolower( untrailingslashit( $link_clean ) ) );
     301
     302        $exists = $wpdb->get_var(
     303            $wpdb->prepare(
     304                "SELECT id FROM $table_name WHERE link_hash = %s LIMIT 1",
     305                $link_hash
     306            )
     307        );
     308
     309        return !empty( $exists );
    690310    } // End already_added()
    691311
     
    693313    /**
    694314     * Add a new broken or warning link
    695      * $args = [
    696      *      'broken'   => true,
    697      *      'warning'  => false,
    698      *      'code'     => 404,
    699      *      'text'     => 'Not found',
    700      *      'link'     => 'https://brokenlink.com',
    701      *      'source'   => 'https://source-url.com',
    702      *      'author'   => 1,
    703      *      'location' => 'content'
    704      * ]
    705315     *
    706316     * @param array $args
     
    708318     */
    709319    public function add( $args ) {
    710         // If already added, no need to add again
    711         if ( $this->already_added( $args[ 'link' ] ) ) {
     320        global $wpdb;
     321
     322        $table_name = $wpdb->prefix . $this->table_name;
     323
     324        $link = sanitize_text_field( $args[ 'link' ] );
     325        $link_normalized = strtolower( untrailingslashit( $link ) );
     326        $link_hash = md5( $link_normalized );
     327
     328        if ( $this->already_added( $link ) ) {
    712329            return 'Link already added';
    713330        }
    714331
    715         // Validate source
    716         $source_url = remove_query_arg( (new BLNOTIFIER_HELPERS)->get_qs_to_remove_from_source(), filter_var( $args[ 'source' ], FILTER_SANITIZE_URL ) );
     332        $source_url = remove_query_arg(
     333            ( new BLNOTIFIER_HELPERS )->get_qs_to_remove_from_source(),
     334            filter_var( $args[ 'source' ], FILTER_SANITIZE_URL )
     335        );
     336
    717337        if ( !$source_url ) {
    718338            return __( 'Invalid source:', 'broken-link-notifier' ) . ' ' . $source_url;
    719339        }
    720340
    721         // Args
    722         $data = [
    723             'post_title'    => sanitize_text_field( $args[ 'link' ] ),
    724             'post_content'  => sanitize_text_field( $args[ 'text' ] ),
    725             'post_status'   => 'publish',
    726             'post_type'     => $this->post_type,
    727             'meta_input'    => [
    728                 'type'     => sanitize_key( $args[ 'type' ] ),
    729                 'code'     => absint( $args[ 'code' ] ),
    730                 'source'   => $source_url,
    731                 'location' => sanitize_key( $args[ 'location' ] ),
    732                 'guest'    => false,
    733                 'method'   => sanitize_key( $args[ 'method' ] )
     341        $inserted = $wpdb->insert(
     342            $table_name,
     343            [
     344                'link'       => $link,
     345                'link_hash'  => $link_hash,
     346                'text'       => sanitize_text_field( $args[ 'text' ] ),
     347                'type'       => sanitize_key( $args[ 'type' ] ),
     348                'code'       => absint( $args[ 'code' ] ),
     349                'source'     => esc_url_raw( $source_url ),
     350                'location'   => sanitize_key( $args[ 'location' ] ),
     351                'method'     => sanitize_key( $args[ 'method' ] ),
     352                'guest'      => ( absint( $args[ 'author' ] ) === 0 ) ? 1 : 0,
     353                'author_id'  => absint( $args[ 'author' ] ),
     354                'created_at' => current_time( 'mysql' ),
    734355            ],
    735         ];
    736 
    737         // Guest or not?
    738         if ( $args[ 'author' ] == 0 ) {
    739             $data[ 'meta_input' ][ 'guest' ] = true;
    740         } else {
    741             $data[ 'post_author' ] = absint( $args[ 'author' ] );
    742         }
    743 
    744         // Add it
    745         $link_id = wp_insert_post( $data );
    746         if ( !is_wp_error( $link_id ) ) {
    747             return $link_id;
    748         } else {
    749             $error = $link_id->get_error_message();
    750             error_log( $error ); // phpcs:ignore
    751             return $error;
    752         }
     356            [ '%s','%s','%s','%s','%d','%s','%s','%s','%d','%d','%s' ]
     357        );
     358
     359        if ( $inserted ) {
     360            return $wpdb->insert_id;
     361        }
     362
     363        return 'Insert failed';
    753364    } // End add()
    754365
     
    760371     * @return boolean
    761372     */
    762     public function remove( $link, $link_id = false ) {
    763         if ( !$link_id ) {
    764             $link_id = post_exists( $link, '', '', $this->post_type );
    765         }
    766         if ( $link_id ) {
    767             if ( get_post_type( $link_id ) == $this->post_type ) {
    768                 if ( wp_delete_post( $link_id ) ) {
    769                     return true;
    770                 }
    771             }
    772         }
    773         return false;
     373    public function remove( $link ) {
     374        global $wpdb;
     375
     376        $table_name = $wpdb->prefix . $this->table_name;
     377        $link_hash = md5( strtolower( untrailingslashit( sanitize_text_field( $link ) ) ) );
     378
     379        $deleted = $wpdb->delete(
     380            $table_name,
     381            [ 'link_hash' => $link_hash ],
     382            [ '%s' ]
     383        );
     384
     385        return ( $deleted > 0 );
    774386    } // End remove()
    775387
     
    928540
    929541    /**
    930      * Ajax call for front end
    931      *
    932      * @return void
     542     * Public AJAX endpoint used by the front-end scanner.
     543     *
     544     * This endpoint intentionally allows unauthenticated requests because the
     545     * plugin scans links from publicly accessible pages visited by guests.
     546     *
     547     * Security protections implemented:
     548     * - Nonce verification to prevent CSRF
     549     * - Rate limiting per IP via transient
     550     * - Maximum links per scan enforced
     551     * - URLs sanitized before processing
     552     * - Only HTTP/HTTPS sources allowed
     553     *
     554     * No privileged actions are performed. The endpoint only scans links and
     555     * records results in a custom table that is not publicly accessible.
    933556     */
    934557    public function ajax_blinks() {
     
    937560            exit( 'No naughty business please.' );
    938561        }
     562
     563        // Public endpoint: allow guests, but validate capability for logged-in users.
     564        if ( is_user_logged_in() && ! current_user_can( 'read' ) ) {
     565            wp_send_json_error( 'Permission denied' );
     566        }
    939567   
    940         // Get the ID
     568        // Get the source and links
    941569        $source_url    = isset( $_REQUEST[ 'source_url' ] ) ? filter_var( wp_unslash( $_REQUEST[ 'source_url' ] ), FILTER_SANITIZE_URL ) : '';
    942570        $header_links  = isset( $_REQUEST[ 'header_links' ] ) ? wp_unslash( $_REQUEST[ 'header_links' ] ) : []; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
     
    1116744
    1117745        // Check permissions
    1118         if ( !(new BLNOTIFIER_HELPERS)->user_can_manage_broken_links() ) {
     746        $HELPERS = new BLNOTIFIER_HELPERS;
     747        if ( !$HELPERS->user_can_manage_broken_links() ) {
    1119748            exit( 'Unauthorized access.' );
    1120749        }
    1121750   
    1122         // Get the ID
     751        // Get the data
    1123752        $link      = isset( $_REQUEST[ 'link' ] ) ? sanitize_text_field( wp_unslash( $_REQUEST[ 'link' ] ) ) : false;
    1124         $post_id   = isset( $_REQUEST[ 'postID' ] ) ? absint( wp_unslash( $_REQUEST[ 'postID' ] ) ) : false;
     753        $link_id   = isset( $_REQUEST[ 'linkID' ] ) ? absint( wp_unslash( $_REQUEST[ 'linkID' ] ) ) : false;
    1125754        $code      = isset( $_REQUEST[ 'code' ] ) ? absint( wp_unslash( $_REQUEST[ 'code' ] ) ) : false;
    1126755        $type      = isset( $_REQUEST[ 'type' ] ) ? sanitize_key( wp_unslash( $_REQUEST[ 'type' ] ) ) : false;
     
    1131760        if ( $link ) {
    1132761
    1133             // Initiate helpers
    1134             $HELPERS = new BLNOTIFIER_HELPERS;
    1135 
    1136762            // If the source no longer exists, auto remove it
    1137763            if ( !$source_id || !get_post( $source_id ) ) {
    1138                 $remove = $this->remove( $HELPERS->str_replace_on_link( $link ), $post_id );
     764                $remove = $this->remove( $HELPERS->str_replace_on_link( $link ) );
    1139765                $status = [
    1140766                    'type' => 'n/a',
     
    1148774                    $result[ 'status' ] = $status;
    1149775                    $result[ 'link' ] = $link;
    1150                     $result[ 'post_id' ] = $post_id;
     776                    $result[ 'link_id' ] = $link_id;
    1151777                } else {
    1152778                    $result[ 'type' ] = 'error';
     
    1162788                // If it's good now, remove the old post
    1163789                if ( $status[ 'type' ] == 'good' || $status[ 'type' ] == 'omitted' ) {
    1164                     $remove = $this->remove( $HELPERS->str_replace_on_link( $link ), $post_id );
     790                    $remove = $this->remove( $HELPERS->str_replace_on_link( $link ) );
    1165791                    if ( $remove ) {
    1166792                        $result[ 'type' ] = 'success';
    1167793                        $result[ 'status' ] = $status;
    1168794                        $result[ 'link' ] = $link;
    1169                         $result[ 'post_id' ] = $post_id;
     795                        $result[ 'link_id' ] = $link_id;
    1170796                    } else {
    1171797                        $result[ 'type' ] = 'error';
     
    1178804                // If it's still not good, but doesn't have the same code or type, update it
    1179805                } elseif ( $code !== $status[ 'code' ] || $type !== $status[ 'type' ] ) {
    1180                     $remove = $this->remove( $HELPERS->str_replace_on_link( $link ), $post_id );
     806                    $remove = $this->remove( $HELPERS->str_replace_on_link( $link ) );
    1181807                    if ( $remove ) {
    1182808                        $result[ 'type' ] = 'success';
    1183809                        $result[ 'status' ] = $status;
    1184810                        $result[ 'link' ] = $link;
    1185                         $result[ 'post_id' ] = $post_id;
     811                        $result[ 'link_id' ] = $link_id;
    1186812   
    1187813                        // Re-add it with new data
     
    1191817                            'text'     => $status[ 'text' ],
    1192818                            'link'     => $status[ 'link' ],
    1193                             'source'   => get_the_permalink( $post_id ),
     819                            'source'   => get_the_permalink( $source_id ),
    1194820                            'author'   => get_current_user_id(),
    1195821                            'location' => 'content',
     
    1204830                    $result[ 'status' ] = $status;
    1205831                    $result[ 'link' ] = $link;
    1206                     $result[ 'post_id' ] = $post_id;
     832                    $result[ 'link_id' ] = $link_id;
    1207833                }
    1208834            }
     
    1230856        }
    1231857
    1232         if ( !(new BLNOTIFIER_HELPERS)->user_can_manage_broken_links() ) {
     858        $HELPERS = new BLNOTIFIER_HELPERS;
     859        if ( !$HELPERS->user_can_manage_broken_links() ) {
    1233860            exit( 'Unauthorized access.' );
    1234861        }
    1235862   
    1236863        // Get the vars
    1237         $result_id  = isset( $_REQUEST[ 'resultID' ] ) ? absint( wp_unslash( $_REQUEST[ 'resultID' ] ) ) : false;
    1238864        $oldLink    = isset( $_REQUEST[ 'oldLink' ] ) ? sanitize_text_field( wp_unslash( $_REQUEST[ 'oldLink' ] ) ) : false;
    1239865        $newLink    = isset( $_REQUEST[ 'newLink' ] ) ? sanitize_text_field( wp_unslash( $_REQUEST[ 'newLink' ] ) ) : false;
    1240866        $source_id  = isset( $_REQUEST[ 'sourceID' ] ) ? absint( wp_unslash( $_REQUEST[ 'sourceID' ] ) ) : false;
    1241867
    1242         if ( $result_id && $oldLink && $newLink && $source_id && get_post( $source_id ) ) {
     868        if ( $oldLink && $newLink && $source_id && get_post( $source_id ) ) {
    1243869
    1244870            // Get the current post content
     
    1259885
    1260886                // Let's also delete the result
    1261                 if ( get_post_type( $result_id ) == $this->post_type ) {
    1262                     wp_delete_post( $result_id );
    1263                 }
     887                $this->remove( $HELPERS->str_replace_on_link( $oldLink ) );
    1264888
    1265889                // Respond
     
    1286910        }
    1287911
    1288         if ( !(new BLNOTIFIER_HELPERS)->user_can_manage_broken_links() ) {
     912        $HELPERS = new BLNOTIFIER_HELPERS;
     913        if ( !$HELPERS->user_can_manage_broken_links() ) {
    1289914            exit( 'Unauthorized access.' );
    1290915        }
    1291916   
    1292         // Get the ID
    1293         $post_id = isset( $_REQUEST[ 'postID' ] ) ? absint( $_REQUEST[ 'postID' ] ) : false;
    1294         if ( $post_id ) {
    1295 
    1296             // Delete it
    1297             if ( get_post_type( $post_id ) == $this->post_type ) {
    1298                 if ( wp_delete_post( $post_id ) ) {
    1299                     wp_send_json_success();
    1300                 }
    1301             }
     917        // Remove the link
     918        $link = isset( $_REQUEST[ 'link' ] ) ? sanitize_text_field( wp_unslash( $_REQUEST[ 'link' ] ) ) : false;
     919        if ( $link ) {
     920            $this->remove( $HELPERS->str_replace_on_link( $link ) );
     921            wp_send_json_success();
    1302922        }
    1303923
     
    1333953
    1334954            // Delete all links with this source
     955            global $wpdb;
     956
    1335957            $source_url = (new BLNOTIFIER_HELPERS)->get_clean_permalink( $source_id );
    1336958            if ( $source_url ) {
    1337                 $query = new WP_Query( [
    1338                     'post_type'      => $this->post_type,
    1339                     'meta_query'     => [ // phpcs:ignore
    1340                         [
    1341                             'key'   => 'source',
    1342                             'value' => $source_url,
    1343                         ]
    1344                     ],
    1345                     'fields'         => 'ids',
    1346                     'posts_per_page' => -1,
    1347                 ] );
    1348    
    1349                 if ( !empty( $query->posts ) ) {
    1350                     foreach ( $query->posts as $post_id ) {
    1351                         if ( get_post_type( $post_id ) == $this->post_type ) {
    1352                             wp_delete_post( $post_id, true );
    1353                         }
    1354                     }
    1355                 }
     959                $table_name = $wpdb->prefix . $this->table_name;
     960
     961                $wpdb->delete(
     962                    $table_name,
     963                    [ 'source' => $source_url ],
     964                    [ '%s' ]
     965                );
    1356966            }
    1357967
     
    14271037     */
    14281038    public function back_script_enqueuer( $screen ) {
    1429         $post_type = get_post_type();
    1430         if ( $screen == 'edit.php' && $post_type == 'blnotifier-results' ) {
     1039        if ( $screen == 'toplevel_page_' . BLNOTIFIER_TEXTDOMAIN ) {
    14311040            $handle = 'blnotifier_results_back_end_script';
    1432             wp_register_script( $handle, BLNOTIFIER_PLUGIN_JS_PATH.'results-back.js', [ 'jquery' ], BLNOTIFIER_VERSION, true );
     1041            wp_register_script( $handle, BLNOTIFIER_PLUGIN_JS_PATH.'results-back.js', [ 'jquery' ], time(), true );
    14331042            wp_localize_script( $handle, 'blnotifier_back_end', [
    14341043                'verifying'     => !(new BLNOTIFIER_HELPERS())->is_results_verification_paused(),
  • broken-link-notifier/trunk/readme.txt

    r3471187 r3475895  
    55Tested up to: 6.9
    66Requires PHP: 7.4
    7 Stable tag: 1.3.6
     7Stable tag: 1.3.7
    88License: GPLv2 or later
    99License URI: http://www.gnu.org/licenses/gpl-2.0.txt
     
    127127
    128128== Changelog ==
     129= 1.3.7 =
     130* Update: Changed storage of broken links from custom post type to custom database table
     131* Update: Add option to cleanup on uninstall
     132* Fix: Error when `blnotifier_link_before_prechecks` hook returns status
     133
    129134= 1.3.6 =
    130135* Update: Added option for Max Links Per Page to help prevent attacks and timeouts
Note: See TracChangeset for help on using the changeset viewer.