Add optional rate limiting
This commit is contained in:
parent
95a7a4b305
commit
f63256326f
@ -821,6 +821,7 @@ define( 'CONSTANT_NAME', value );
|
||||
| FICTIONEER_CHAPTER_FOLDING_THRESHOLD | integer | Threshold before and after folding in chapter lists. Default `5`.
|
||||
| FICTIONEER_SHORTCODE_TRANSIENT_EXPIRATION | integer | Expiration duration for shortcode Transients in seconds. Default `300`.
|
||||
| FICTIONEER_STORY_COMMENT_COUNT_TIMEOUT | integer | Timeout between comment count refreshes for stories in seconds. Default `900`.
|
||||
| FICTIONEER_REQUESTS_PER_MINUTE | integer | Maximum requests per minute if the rate limit is enabled. Default `5`.
|
||||
| FICTIONEER_CACHE_PURGE_ASSIST | boolean | Whether to call the cache purge assist function on post updates. Default `true`.
|
||||
| FICTIONEER_RELATIONSHIP_PURGE_ASSIST | boolean | Whether to purge related post caches. Default `true`.
|
||||
| FICTIONEER_CHAPTER_LIST_TRANSIENTS | boolean | Whether to cache chapter lists on story pages as Transients. Default `true`.
|
||||
|
@ -241,6 +241,11 @@ if ( ! defined( 'FICTIONEER_STORY_COMMENT_COUNT_TIMEOUT' ) ) {
|
||||
define( 'FICTIONEER_STORY_COMMENT_COUNT_TIMEOUT', 900 );
|
||||
}
|
||||
|
||||
// Integer: Requests per minute
|
||||
if ( ! defined( 'FICTIONEER_REQUESTS_PER_MINUTE' ) ) {
|
||||
define( 'FICTIONEER_REQUESTS_PER_MINUTE', 5 );
|
||||
}
|
||||
|
||||
/*
|
||||
* Booleans
|
||||
*/
|
||||
|
@ -574,6 +574,56 @@ if ( ! function_exists( 'fictioneer_get_validated_ajax_user' ) ) {
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// RATE LIMIT
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Checks rate limit globally or for an action via the session
|
||||
*
|
||||
* @since Fictioneer 5.7.1
|
||||
*
|
||||
* @param string $action The action to check for rate-limiting.
|
||||
* Defaults to 'fictioneer_global'.
|
||||
*/
|
||||
|
||||
function fictioneer_check_rate_limit( $action = 'fictioneer_global' ) {
|
||||
if ( ! get_option( 'fictioneer_enable_rate_limits' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Start session if not already done
|
||||
if ( session_status() == PHP_SESSION_NONE ) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
// Initialize if not set
|
||||
if ( ! isset( $_SESSION[ $action ]['request_times'] ) ) {
|
||||
$_SESSION[ $action ]['request_times'] = [];
|
||||
}
|
||||
|
||||
// Setup
|
||||
$current_time = microtime( true );
|
||||
$time_window = 60;
|
||||
|
||||
// Filter out old timestamps
|
||||
$_SESSION[ $action ]['request_times'] = array_filter(
|
||||
$_SESSION[ $action ]['request_times'],
|
||||
function ( $time ) use ( $current_time, $time_window ) {
|
||||
return ( $current_time - $time ) < $time_window;
|
||||
}
|
||||
);
|
||||
|
||||
// Limit exceeded?
|
||||
if ( count( $_SESSION[ $action ]['request_times'] ) >= FICTIONEER_REQUESTS_PER_MINUTE ) {
|
||||
http_response_code( 429 ); // Too many requests
|
||||
exit;
|
||||
}
|
||||
|
||||
// Record the current request time
|
||||
$_SESSION[ $action ]['request_times'][] = $current_time;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// KEY/VALUE STRING REPLACEMENT
|
||||
// =============================================================================
|
||||
|
@ -621,6 +621,13 @@ define( 'FICTIONEER_OPTIONS', array(
|
||||
'sanitize_callback' => 'fictioneer_sanitize_checkbox',
|
||||
'label' => __( 'Enable fast AJAX for comments', 'fictioneer' ),
|
||||
'default' => false
|
||||
),
|
||||
'fictioneer_enable_rate_limits' => array(
|
||||
'name' => 'fictioneer_enable_rate_limits',
|
||||
'group' => 'fictioneer-settings-general-group',
|
||||
'sanitize_callback' => 'fictioneer_sanitize_checkbox',
|
||||
'label' => __( 'Enable rate limiting for AJAX requests', 'fictioneer' ),
|
||||
'default' => false
|
||||
)
|
||||
),
|
||||
'integers' => array(
|
||||
|
@ -758,6 +758,19 @@
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label for="fictioneer_enable_rate_limits" class="label-wrapped-checkbox row">
|
||||
<input name="fictioneer_enable_rate_limits" type="checkbox" id="fictioneer_enable_rate_limits" <?php echo checked( 1, get_option( 'fictioneer_enable_rate_limits' ), false ); ?> value="1">
|
||||
<div>
|
||||
<span><?php echo FICTIONEER_OPTIONS['booleans']['fictioneer_enable_rate_limits']['label']; ?></span>
|
||||
<p class="sub-label"><?php
|
||||
printf(
|
||||
__( 'Simple session-based rate limiting, allowing <code>%s</code> requests per minute for selected actions (per action).', 'fictioneer' ),
|
||||
FICTIONEER_REQUESTS_PER_MINUTE
|
||||
);
|
||||
?></p>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label for="fictioneer_disable_application_passwords" class="label-wrapped-checkbox row">
|
||||
<input name="fictioneer_disable_application_passwords" type="checkbox" id="fictioneer_disable_application_passwords" <?php echo checked( 1, get_option( 'fictioneer_disable_application_passwords' ), false ); ?> value="1">
|
||||
<div>
|
||||
|
@ -258,10 +258,14 @@ if ( ! function_exists( 'fictioneer_query_followed_chapters' ) ) {
|
||||
* Sends the HTML for Follows notifications via AJAX
|
||||
*
|
||||
* @since Fictioneer 4.3
|
||||
* @see fictioneer_check_rate_limit()
|
||||
* @see fictioneer_get_validated_ajax_user()
|
||||
*/
|
||||
|
||||
function fictioneer_ajax_get_follows_notifications() {
|
||||
// Rate limit
|
||||
fictioneer_check_rate_limit( 'fictioneer_ajax_get_follows_notifications' );
|
||||
|
||||
// Setup and validations
|
||||
$user = fictioneer_get_validated_ajax_user();
|
||||
|
||||
|
2
js/follows.min.js
vendored
2
js/follows.min.js
vendored
@ -1 +1 @@
|
||||
const fcn_desktopFollowList=_$$$("follow-menu-scroll"),fcn_mobileFollowList=_$$$("mobile-menu-follows-list"),fcn_followsMenuItem=_$$$("follow-menu-button");var fcn_userFollowsTimeout,fcn_follows;function fcn_initializeFollows(o){const t=o.detail.data.follows;!1!==t&&(fcn_follows=t,fcn_updateFollowsView(),localStorage.removeItem("fcnBookshelfContent"))}function fcn_toggleFollow(o){const t=fcn_getUserData();if(fcn_follows&&t.follows){if(localStorage.removeItem("fcnBookshelfContent"),JSON.stringify(fcn_follows.data[o])!==JSON.stringify(t.follows.data[o]))return fcn_follows=t.follows,fcn_showNotification(__("Follows re-synchronized.","fictioneer")),void fcn_updateFollowsView();fcn_follows.data.hasOwnProperty(o)?delete fcn_follows.data[o]:fcn_follows.data[o]={story_id:parseInt(o),timestamp:Date.now()},t.follows.data[o]=fcn_follows.data[o],t.lastLoaded=0,fcn_setUserData(t),fcn_updateFollowsView(),clearTimeout(fcn_userFollowsTimeout),fcn_userFollowsTimeout=setTimeout((()=>{fcn_ajaxPost({action:"fictioneer_ajax_toggle_follow",fcn_fast_ajax:1,story_id:o,set:fcn_follows.data.hasOwnProperty(o)}).then((o=>{o.data.error&&fcn_showNotification(o.data.error,5,"warning")})).catch((o=>{o.status&&o.statusText&&fcn_showNotification(`${o.status}: ${o.statusText}`,5,"warning")}))}),fictioneer_ajax.post_debounce_rate)}}function fcn_updateFollowsView(){const o=fcn_getUserData();if(!fcn_follows||!o.follows)return;_$$(".button-follow-story").forEach((o=>{o.classList.toggle("_followed",fcn_follows?.data.hasOwnProperty(o.dataset.storyId))})),_$$(".card").forEach((o=>{o.classList.toggle("has-follow",fcn_follows?.data.hasOwnProperty(o.dataset.storyId))}));const t=parseInt(fcn_follows.new)>0;_$$(".mark-follows-read, .follows-alert-number, .mobile-menu-button").forEach((o=>{o.classList.toggle("_new",t),t>0&&(o.dataset.newCount=fcn_follows.new)}))}function fcn_setupFollowsHTML(){fcn_followsMenuItem.classList.contains("_loaded")||fcn_ajaxGet({action:"fictioneer_ajax_get_follows_notifications",fcn_fast_ajax:1}).then((o=>{o.data.html&&(fcn_desktopFollowList.innerHTML=o.data.html,fcn_mobileFollowList.innerHTML=o.data.html)})).catch((o=>{o.status&&o.statusText&&fcn_showNotification(`${o.status}: ${o.statusText}`,5,"warning"),fcn_desktopFollowList.remove(),fcn_mobileFollowList.remove()})).then((()=>{fcn_followsMenuItem.classList.add("_loaded")}))}function fcn_markFollowsRead(){if(!fcn_followsMenuItem.classList.contains("_new")||!fcn_followsMenuItem.classList.contains("_loaded"))return;_$$(".mark-follows-read, .follows-alert-number, .follow-item, .mobile-menu-button").forEach((o=>{o.classList.remove("_new")}));const o=fcn_getUserData();o.new=0,o.lastLoaded=0,fcn_setUserData(o),fcn_ajaxPost({action:"fictioneer_ajax_mark_follows_read",fcn_fast_ajax:1}).catch((o=>{o.status&&o.statusText&&fcn_showNotification(`${o.status}: ${o.statusText}`,5,"warning")}))}document.addEventListener("fcnUserDataReady",(o=>{fcn_initializeFollows(o)})),fcn_followsMenuItem?.addEventListener("mouseover",(()=>{fcn_setupFollowsHTML()}),{once:!0}),fcn_followsMenuItem?.addEventListener("focus",(()=>{fcn_setupFollowsHTML()}),{once:!0}),_$('.mobile-menu__frame-button[data-frame-target="follows"]')?.addEventListener("click",(()=>{fcn_setupFollowsHTML()}),{once:!0}),_$$(".button-follow-story").forEach((o=>{o.addEventListener("click",(o=>{fcn_toggleFollow(o.currentTarget.dataset.storyId)}))})),_$$(".mark-follows-read").forEach((o=>{o.addEventListener("click",(()=>{fcn_markFollowsRead()}))}));
|
||||
const fcn_desktopFollowList=_$$$("follow-menu-scroll"),fcn_mobileFollowList=_$$$("mobile-menu-follows-list"),fcn_followsMenuItem=_$$$("follow-menu-button");var fcn_userFollowsTimeout,fcn_follows;function fcn_initializeFollows(o){const t=o.detail.data.follows;!1!==t&&(fcn_follows=t,fcn_updateFollowsView(),localStorage.removeItem("fcnBookshelfContent"))}function fcn_toggleFollow(o){const t=fcn_getUserData();if(fcn_follows&&t.follows){if(localStorage.removeItem("fcnBookshelfContent"),JSON.stringify(fcn_follows.data[o])!==JSON.stringify(t.follows.data[o]))return fcn_follows=t.follows,fcn_showNotification(__("Follows re-synchronized.","fictioneer")),void fcn_updateFollowsView();fcn_follows.data.hasOwnProperty(o)?delete fcn_follows.data[o]:fcn_follows.data[o]={story_id:parseInt(o),timestamp:Date.now()},t.follows.data[o]=fcn_follows.data[o],t.lastLoaded=0,fcn_setUserData(t),fcn_updateFollowsView(),clearTimeout(fcn_userFollowsTimeout),fcn_userFollowsTimeout=setTimeout((()=>{fcn_ajaxPost({action:"fictioneer_ajax_toggle_follow",fcn_fast_ajax:1,story_id:o,set:fcn_follows.data.hasOwnProperty(o)}).then((o=>{o.data.error&&fcn_showNotification(o.data.error,5,"warning")})).catch((o=>{o.status&&o.statusText&&fcn_showNotification(`${o.status}: ${o.statusText}`,5,"warning")}))}),fictioneer_ajax.post_debounce_rate)}}function fcn_updateFollowsView(){const o=fcn_getUserData();if(!fcn_follows||!o.follows)return;_$$(".button-follow-story").forEach((o=>{o.classList.toggle("_followed",fcn_follows?.data.hasOwnProperty(o.dataset.storyId))})),_$$(".card").forEach((o=>{o.classList.toggle("has-follow",fcn_follows?.data.hasOwnProperty(o.dataset.storyId))}));const t=parseInt(fcn_follows.new)>0;_$$(".mark-follows-read, .follows-alert-number, .mobile-menu-button").forEach((o=>{o.classList.toggle("_new",t),t>0&&(o.dataset.newCount=fcn_follows.new)}))}function fcn_setupFollowsHTML(){fcn_followsMenuItem.classList.contains("_loaded")||fcn_ajaxGet({action:"fictioneer_ajax_get_follows_notifications",fcn_fast_ajax:1}).then((o=>{o.data.html&&(fcn_desktopFollowList.innerHTML=o.data.html,fcn_mobileFollowList.innerHTML=o.data.html)})).catch((o=>{429===o.status?fcn_showNotification(__("Slow down.","fcnl"),3,"warning"):o.status&&o.statusText&&fcn_showNotification(`${o.status}: ${o.statusText}`,5,"warning"),fcn_desktopFollowList.remove(),fcn_mobileFollowList.remove()})).then((()=>{fcn_followsMenuItem.classList.add("_loaded")}))}function fcn_markFollowsRead(){if(!fcn_followsMenuItem.classList.contains("_new")||!fcn_followsMenuItem.classList.contains("_loaded"))return;_$$(".mark-follows-read, .follows-alert-number, .follow-item, .mobile-menu-button").forEach((o=>{o.classList.remove("_new")}));const o=fcn_getUserData();o.new=0,o.lastLoaded=0,fcn_setUserData(o),fcn_ajaxPost({action:"fictioneer_ajax_mark_follows_read",fcn_fast_ajax:1}).catch((o=>{o.status&&o.statusText&&fcn_showNotification(`${o.status}: ${o.statusText}`,5,"warning")}))}document.addEventListener("fcnUserDataReady",(o=>{fcn_initializeFollows(o)})),fcn_followsMenuItem?.addEventListener("mouseover",(()=>{fcn_setupFollowsHTML()}),{once:!0}),fcn_followsMenuItem?.addEventListener("focus",(()=>{fcn_setupFollowsHTML()}),{once:!0}),_$('.mobile-menu__frame-button[data-frame-target="follows"]')?.addEventListener("click",(()=>{fcn_setupFollowsHTML()}),{once:!0}),_$$(".button-follow-story").forEach((o=>{o.addEventListener("click",(o=>{fcn_toggleFollow(o.currentTarget.dataset.storyId)}))})),_$$(".mark-follows-read").forEach((o=>{o.addEventListener("click",(()=>{fcn_markFollowsRead()}))}));
|
@ -193,7 +193,9 @@ function fcn_setupFollowsHTML() {
|
||||
})
|
||||
.catch(error => {
|
||||
// Show server error
|
||||
if (error.status && error.statusText) {
|
||||
if (error.status === 429) {
|
||||
fcn_showNotification(__( 'Slow down.', 'fcnl' ), 3, 'warning');
|
||||
} else if (error.status && error.statusText) {
|
||||
fcn_showNotification(`${error.status}: ${error.statusText}`, 5, 'warning');
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user