| @@ -10,8 +10,43 @@ | ||
| 10 | 10 | use WPDeveloper\BetterDocs\Core\BaseAPI; |
| 11 | 11 | |
| 12 | 12 | class Feedback extends BaseAPI { |
| 13 | 13 | /** |
| 14 | + * The reaction/feedback beacon is public (logged-out visitors react), so it is | |
| 15 | + * gated by a wp_rest nonce the frontend sends via the X-WP-Nonce header — | |
| 16 | + * mirroring the analytics view beacon ({@see REST\AnalyticsTracker}). Without | |
| 17 | + * this it inherited BaseAPI::permission_check() (return true) and could be | |
| 18 | + * scripted anonymously to forge reaction counts and flood the Pro feedback | |
| 19 | + * table (one row per call via the betterdocs_feedback_recorded action). A light | |
| 20 | + * salted-IP throttle bounds abuse even if a nonce is harvested from a page. | |
| 21 | + */ | |
| 22 | + public function permission_check( $request = null ) { | |
| 23 | + if ( ! $request instanceof WP_REST_Request ) { | |
| 24 | + return false; | |
| 25 | + } | |
| 26 | + | |
| 27 | + $nonce = $request->get_header( 'x_wp_nonce' ); | |
| 28 | + if ( empty( $nonce ) ) { | |
| 29 | + $nonce = $request->get_param( '_wpnonce' ); | |
| 30 | + } | |
| 31 | + if ( ! wp_verify_nonce( $nonce, 'wp_rest' ) ) { | |
| 32 | + return false; | |
| 33 | + } | |
| 34 | + | |
| 35 | + // Defense-in-depth: cap reactions per client (salted IP hash, raw IP never | |
| 36 | + // stored) so a harvested nonce can't be scripted into a table flood. | |
| 37 | + $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : ''; | |
| 38 | + $key = 'bd_feedback_rl_' . substr( wp_hash( $ip ), 0, 20 ); | |
| 39 | + $hits = (int) get_transient( $key ); | |
| 40 | + if ( $hits >= 120 ) { | |
| 41 | + return new \WP_Error( 'bd_feedback_throttled', __( 'Too many reactions — please try again in a moment.', 'betterdocs' ), [ 'status' => 429 ] ); | |
| 42 | + } | |
| 43 | + set_transient( $key, $hits + 1, 10 * MINUTE_IN_SECONDS ); | |
| 44 | + | |
| 45 | + return true; | |
| 46 | + } | |
| 47 | + | |
| 48 | + /** | |
| 14 | 49 | * @return mixed |
| 15 | 50 | */ |
| 16 | 51 | public function register() { |
| 17 | 52 | $this->post( |
| @@ -196,8 +231,17 @@ | ||
| 196 | 231 | ); |
| 197 | 232 | } |
| 198 | 233 | |
| 199 | 234 | if ( $insert == true ) { |
| 235 | + /** | |
| 236 | + * Fires after a reaction is recorded into the daily aggregate. | |
| 237 | + * Pro hooks this to write a per-item row into the feedback inbox | |
| 238 | + * table (betterdocs_analytics_feedback). | |
| 239 | + * | |
| 240 | + * @param int $docs_id Doc post id. | |
| 241 | + * @param string $feelings happy|sad|normal. | |
| 242 | + */ | |
| 243 | + do_action( 'betterdocs_feedback_recorded', (int) $docs_id, $feelings ); | |
| 200 | 244 | return true; |
| 201 | 245 | } |
| 202 | 246 | } |
| 203 | 247 | return false; |