--- a/broken-link-notifier/broken-link-notifier.php
+++ b/broken-link-notifier/broken-link-notifier.php
@@ -3,9 +3,9 @@
* Plugin Name: Broken Link Notifier
* Plugin URI: https://pluginrx.com/plugin/broken-link-notifier/
* Description: Get notified when someone loads a page with a broken link
- * Version: 1.3.5
+ * Version: 1.3.6
* Requires at least: 5.9
- * Tested up to: 6.8
+ * Tested up to: 6.9
* Requires PHP: 7.4
* Author: PluginRx
* Author URI: https://pluginrx.com/
--- a/broken-link-notifier/includes/helpers.php
+++ b/broken-link-notifier/includes/helpers.php
@@ -15,6 +15,40 @@
class BLNOTIFIER_HELPERS {
/**
+ * Check if the current user can manage Broken Link Notifier.
+ *
+ * @return bool True if user has permission, false otherwise.
+ */
+ public function user_can_manage_broken_links() : bool {
+ if ( ! is_user_logged_in() ) {
+ return false;
+ }
+
+ $current_user = wp_get_current_user();
+ $user_roles = (array) $current_user->roles;
+
+ // Administrators always have access
+ if ( in_array( 'administrator', $user_roles, true ) ) {
+ return true;
+ }
+
+ // Check against allowed roles stored in plugin options
+ $allowed_roles = get_option( 'blnotifier_editable_roles', [] );
+
+ if ( is_array( $allowed_roles ) && ! empty( $allowed_roles ) ) {
+ foreach ( $allowed_roles as $role_slug => $value ) {
+ $role_slug = sanitize_key( $role_slug );
+ if ( in_array( $role_slug, $user_roles, true ) ) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ } // End user_can_manage_broken_links()
+
+
+ /**
* Get the current tab
*
* @return string|false
--- a/broken-link-notifier/includes/menu.php
+++ b/broken-link-notifier/includes/menu.php
@@ -400,7 +400,14 @@
'default' => 5,
'min' => 0,
'comments' => 'Maximum number of redirects before giving up on a link (will only be used if you allow redirects below)'
- ]
+ ],
+ [
+ 'name' => 'max_links_per_page',
+ 'label' => 'Max Links Per Page',
+ 'default' => 200,
+ 'min' => 0,
+ 'comments' => 'Maximum number of links to check per page (0 for unlimited) - this is to prevent attacks and timeouts on pages with a large number of links'
+ ],
];
// Loop through the array to add number fields
--- a/broken-link-notifier/includes/omits.php
+++ b/broken-link-notifier/includes/omits.php
@@ -77,7 +77,6 @@
// Ajax
add_action( 'wp_ajax_'.$this->ajax_key, [ $this, 'ajax' ] );
- add_action( 'wp_ajax_nopriv_'.$this->ajax_key, [ $this, 'must_login' ] );
// Enqueue script
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
@@ -371,6 +370,9 @@
if ( !wp_verify_nonce( $nonce, $this->nonce ) ) {
exit( 'No naughty business please.' );
}
+ if ( !(new BLNOTIFIER_HELPERS)->user_can_manage_broken_links() ) {
+ exit( 'Unauthorized access.' );
+ }
// Get parameters safely
$link = isset( $_REQUEST[ 'link' ] ) ? sanitize_text_field( wp_unslash( $_REQUEST[ 'link' ] ) ) : '';
@@ -407,16 +409,6 @@
/**
- * What to do if they are not logged in
- *
- * @return void
- */
- public function must_login() {
- die();
- } // End must_login()
-
-
- /**
* Enqueue script
*
* @param string $screen
--- a/broken-link-notifier/includes/results.php
+++ b/broken-link-notifier/includes/results.php
@@ -108,14 +108,9 @@
add_action( 'wp_ajax_'.$this->ajax_key_blinks, [ $this, 'ajax_blinks' ] );
add_action( 'wp_ajax_nopriv_'.$this->ajax_key_blinks, [ $this, 'ajax_blinks' ] );
add_action( 'wp_ajax_'.$this->ajax_key_rescan, [ $this, 'ajax_rescan' ] );
- add_action( 'wp_ajax_nopriv_'.$this->ajax_key_rescan, [ $this, 'ajax_rescan' ] );
-
add_action( 'wp_ajax_'.$this->ajax_key_replace_link, [ $this, 'ajax_replace_link' ] );
- add_action( 'wp_ajax_nopriv_'.$this->ajax_key_replace_link, [ $this, 'ajax_unauthorized' ] );
add_action( 'wp_ajax_'.$this->ajax_key_delete_result, [ $this, 'ajax_delete_result' ] );
- add_action( 'wp_ajax_nopriv_'.$this->ajax_key_delete_result, [ $this, 'ajax_unauthorized' ] );
add_action( 'wp_ajax_'.$this->ajax_key_delete_source, [ $this, 'ajax_delete_source' ] );
- add_action( 'wp_ajax_nopriv_'.$this->ajax_key_delete_source, [ $this, 'ajax_unauthorized' ] );
// Enqueue scripts
add_action( 'wp_enqueue_scripts', [ $this, 'front_script_enqueuer' ] );
@@ -948,6 +943,31 @@
$content_links = isset( $_REQUEST[ 'content_links' ] ) ? wp_unslash( $_REQUEST[ 'content_links' ] ) : []; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$footer_links = isset( $_REQUEST[ 'footer_links' ] ) ? wp_unslash( $_REQUEST[ 'footer_links' ] ) : []; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+ // Enforce max links per page
+ $max_links = absint( get_option( 'blnotifier_max_links_per_page', 200 ) );
+ $total_links = count( $header_links ) + count( $content_links ) + count( $footer_links );
+ if ( $total_links > $max_links ) {
+ $result = [
+ 'type' => 'error',
+ 'msg' => sprintf( 'Too many links in one scan. Max allowed: %d', $max_links )
+ ];
+ self::send_ajax_or_redirect( $result );
+ }
+
+ // Rate limit per IP only for non-link-managers
+ if ( !(new BLNOTIFIER_HELPERS)->user_can_manage_broken_links() ) {
+ $ip = $_SERVER[ 'REMOTE_ADDR' ];
+ $transient_key = 'bln_rate_' . md5( $ip );
+ if ( get_transient( $transient_key ) ) {
+ $result = [
+ 'type' => 'error',
+ 'msg' => 'Scan rate limit exceeded'
+ ];
+ self::send_ajax_or_redirect( $result );
+ }
+ set_transient( $transient_key, 1, 10 ); // 10-second cooldown
+ }
+
// Make sure we have a source URL
if ( $source_url ) {
@@ -1079,15 +1099,7 @@
}
// Echo the result or redirect
- if ( !empty( $_SERVER[ 'HTTP_X_REQUESTED_WITH' ] ) && strtolower( sanitize_key( wp_unslash( $_SERVER[ 'HTTP_X_REQUESTED_WITH' ] ) ) ) === 'xmlhttprequest' ) {
- echo wp_json_encode( $result );
- } else {
- $referer = isset( $_SERVER[ 'HTTP_REFERER' ] ) ? filter_var( wp_unslash( $_SERVER[ 'HTTP_REFERER' ] ), FILTER_SANITIZE_URL ) : '';
- header( 'Location: ' . $referer );
- }
-
- // We're done here
- die();
+ self::send_ajax_or_redirect( $result );
} // End ajax_blinks()
@@ -1101,6 +1113,11 @@
if ( !isset( $_REQUEST[ 'nonce' ] ) || !wp_verify_nonce( sanitize_text_field( wp_unslash ( $_REQUEST[ 'nonce' ] ) ), $this->nonce_rescan ) ) {
exit( 'No naughty business please.' );
}
+
+ // Check permissions
+ if ( !(new BLNOTIFIER_HELPERS)->user_can_manage_broken_links() ) {
+ exit( 'Unauthorized access.' );
+ }
// Get the ID
$link = isset( $_REQUEST[ 'link' ] ) ? sanitize_text_field( wp_unslash( $_REQUEST[ 'link' ] ) ) : false;
@@ -1197,30 +1214,11 @@
}
// Echo the result or redirect
- if ( !empty( $_SERVER[ 'HTTP_X_REQUESTED_WITH' ] ) && strtolower( sanitize_key( wp_unslash( $_SERVER[ 'HTTP_X_REQUESTED_WITH' ] ) ) ) === 'xmlhttprequest' ) {
- echo wp_json_encode( $result );
- } else {
- $referer = isset( $_SERVER[ 'HTTP_REFERER' ] ) ? filter_var( wp_unslash( $_SERVER[ 'HTTP_REFERER' ] ), FILTER_SANITIZE_URL ) : '';
- header( 'Location: ' . $referer );
- }
-
- // We're done here
- die();
+ self::send_ajax_or_redirect( $result );
} // End ajax_rescan()
/**
- * Unauthorized ajax
- *
- * @return void
- */
- public function ajax_unauthorized() {
- wp_send_json_error( 'Unauthorized access.', 403 );
- exit;
- } // End ajax_unauthorized()
-
-
- /**
* Ajax call for back end
*
* @return void
@@ -1230,6 +1228,10 @@
if ( !isset( $_REQUEST[ 'nonce' ] ) || !wp_verify_nonce( sanitize_text_field( wp_unslash ( $_REQUEST[ 'nonce' ] ) ), $this->nonce_replace ) ) {
exit( 'No naughty business please.' );
}
+
+ if ( !(new BLNOTIFIER_HELPERS)->user_can_manage_broken_links() ) {
+ exit( 'Unauthorized access.' );
+ }
// Get the vars
$result_id = isset( $_REQUEST[ 'resultID' ] ) ? absint( wp_unslash( $_REQUEST[ 'resultID' ] ) ) : false;
@@ -1282,6 +1284,10 @@
if ( !isset( $_REQUEST[ 'nonce' ] ) || !wp_verify_nonce( sanitize_text_field( wp_unslash ( $_REQUEST[ 'nonce' ] ) ), $this->nonce_delete ) ) {
exit( 'No naughty business please.' );
}
+
+ if ( !(new BLNOTIFIER_HELPERS)->user_can_manage_broken_links() ) {
+ exit( 'Unauthorized access.' );
+ }
// Get the ID
$post_id = isset( $_REQUEST[ 'postID' ] ) ? absint( $_REQUEST[ 'postID' ] ) : false;
@@ -1311,6 +1317,11 @@
exit( 'No naughty business please.' );
}
+ // Check permissions
+ if ( !(new BLNOTIFIER_HELPERS)->user_can_manage_broken_links() ) {
+ exit( 'Unauthorized access.' );
+ }
+
// Make sure we are allowed to delete the source
if ( !get_option( 'blnotifier_enable_delete_source' ) ) {
wp_send_json_error( 'Deleting source is not enabled.' );
@@ -1356,6 +1367,25 @@
/**
+ * Send JSON result or redirect for non-AJAX requests.
+ *
+ * @param array $result The result array to return.
+ *
+ * @return void
+ */
+ public static function send_ajax_or_redirect( $result ) {
+ if ( !empty( $_SERVER[ 'HTTP_X_REQUESTED_WITH' ] ) && strtolower( sanitize_key( wp_unslash( $_SERVER[ 'HTTP_X_REQUESTED_WITH' ] ) ) ) === 'xmlhttprequest' ) {
+ header( 'Content-Type: application/json; charset=' . get_option( 'blog_charset' ) );
+ echo wp_json_encode( $result );
+ } else {
+ $referer = isset( $_SERVER[ 'HTTP_REFERER' ] ) ? filter_var( wp_unslash( $_SERVER[ 'HTTP_REFERER' ] ), FILTER_SANITIZE_URL ) : '';
+ header( 'Location: ' . $referer );
+ }
+ die();
+ } // End send_ajax_or_redirect()
+
+
+ /**
* Enque the JavaScript
*
* @return void
--- a/broken-link-notifier/includes/scan-multi.php
+++ b/broken-link-notifier/includes/scan-multi.php
@@ -52,12 +52,7 @@
* @return boolean
*/
public function has_access() {
- $roles = get_option( 'blnotifier_editable_roles', [] );
- $roles[] = 'administrator';
- if ( !is_user_logged_in() || !array_intersect( wp_get_current_user()->roles, $roles ) ) {
- return false;
- }
- return true;
+ return (new BLNOTIFIER_HELPERS)->user_can_manage_broken_links();
} // End has_access()
--- a/broken-link-notifier/includes/scan.php
+++ b/broken-link-notifier/includes/scan.php
@@ -45,7 +45,6 @@
// Ajax
add_action( 'wp_ajax_'.$this->ajax_key, [ $this, 'ajax' ] );
- add_action( 'wp_ajax_nopriv_'.$this->ajax_key, [ $this, 'must_login' ] );
// Enqueue script
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
@@ -64,6 +63,11 @@
exit( 'No naughty business please.' );
}
+ // Check permissions
+ if ( !(new BLNOTIFIER_HELPERS)->user_can_manage_broken_links() ) {
+ exit( 'Unauthorized access.' );
+ }
+
// Initiate helpers
$HELPERS = new BLNOTIFIER_HELPERS;
@@ -121,16 +125,6 @@
/**
- * What to do if they are not logged in
- *
- * @return void
- */
- public function must_login() {
- die();
- } // End must_login()
-
-
- /**
* Enqueue script
*
* @param string $screen