[ requests, seconds ]. * * @return array */ public static function limits() { $ns = '/' . Routes::NS; /** * Filters the public rate limits. Patterns match the start of the route. * * @param array $limits Route prefix => [ requests, seconds ] (0 requests = no limit). */ return (array) apply_filters( 'kdocs_rate_limits', array( $ns . '/search' => array( 60, MINUTE_IN_SECONDS ), $ns . '/feedback' => array( 10, 10 * MINUTE_IN_SECONDS ), $ns . '/view' => array( 120, HOUR_IN_SECONDS ), $ns . '/public/' => array( 300, MINUTE_IN_SECONDS ), $ns . '/article/' => array( 120, MINUTE_IN_SECONDS ), $ns . '/versions' => array( 120, MINUTE_IN_SECONDS ), $ns . '/import/github-sync/webhook' => array( 30, MINUTE_IN_SECONDS ), $ns . '/integrations/slack/command' => array( 60, MINUTE_IN_SECONDS ), $ns . '/integrations/discord' => array( 60, MINUTE_IN_SECONDS ), ) ); } /** * Limit for a route. * * @param string $route Route. * @return array{0:int,1:int,2:string}|null Requests, seconds, matched prefix. */ public static function limit_for( $route ) { foreach ( self::limits() as $prefix => $limit ) { if ( 0 === strpos( (string) $route, $prefix ) && (int) $limit[0] > 0 ) { return array( (int) $limit[0], max( 1, (int) $limit[1] ), $prefix ); } } return null; } /** * Counts a request. Returns the seconds to wait when over the limit. * * @param string $bucket Bucket. * @param int $max Requests per window. * @param int $window Window in seconds. * @return int 0 when allowed. */ public static function hit( $bucket, $max, $window ) { $now = time(); $start = $now - ( $now % $window ); $key = 'kdocs_rl_' . md5( $bucket . '|' . PublicController::visitor_hash() . '|' . $start ); $count = (int) get_transient( $key ); if ( $count >= $max ) { return max( 1, $start + $window - $now ); } set_transient( $key, $count + 1, $window ); return 0; } /** * `rest_pre_dispatch`: rejects requests over the limit. * * @param mixed $result Response so far. * @param \WP_REST_Server $server Server. * @param \WP_REST_Request $request Request. * @return mixed */ public static function check( $result, $server, $request ) { if ( null !== $result || current_user_can( 'edit_posts' ) ) { return $result; } $route = (string) $request->get_route(); $limit = self::limit_for( $route ); if ( ! $limit ) { return $result; } // One bucket per prefix, so /article/1, /article/2 ... share a limit. $wait = self::hit( $limit[2] . '|' . $request->get_method(), $limit[0], $limit[1] ); if ( ! $wait ) { return $result; } $error = new \WP_Error( 'kdocs_rate_limited', __( 'Too many requests. Please try again shortly.', 'karma-docs' ), array( 'status' => 429 ) ); $response = rest_convert_error_to_response( $error ); $response->header( 'Retry-After', (string) $wait ); return $response; } }