WP_REST_Server::CREATABLE, 'callback' => [ $this, 'log_generation' ], 'permission_callback' => [ $this, 'permission_check' ], ], ] ); } /** * Permission check callback. */ public function permission_check( $request ) { $params = $request->get_json_params(); if ( empty( $params ) ) { $params = json_decode( $request->get_body(), true ); } $template_id = isset( $params['templateId'] ) ? $params['templateId'] : ''; $expires = $request->get_param( 'expires' ); $signature = $request->get_param( 'sig' ); if ( empty( $template_id ) || empty( $expires ) || empty( $signature ) ) { return new WP_Error( '100prints_unauthorized', 'Missing signature or template ID.', [ 'status' => 403 ] ); } if ( time() > (int) $expires ) { return new WP_Error( '100prints_expired', 'Request expired.', [ 'status' => 403 ] ); } $expected = hash_hmac( 'sha256', $template_id . '|' . $expires . '|log', wp_salt( 'auth' ) ); if ( ! hash_equals( $expected, $signature ) ) { return new WP_Error( '100prints_forbidden', 'Invalid signature.', [ 'status' => 403 ] ); } // Rate limiting by IP — fixed-window (60 s window, max 30 requests). // Using a fixed window ensures the block always expires within one window // period, even if a scanner hammers the endpoint continuously. $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : 'unknown'; $transient_key = '100p_limit_log_' . md5( $ip ); $window_size = MINUTE_IN_SECONDS; // 60 seconds $limit = 30; $now = time(); $data = get_transient( $transient_key ); // If no data or the window has expired naturally, start a fresh window. if ( ! is_array( $data ) || ( $now - $data['start'] ) >= $window_size ) { $data = [ 'start' => $now, 'count' => 0, ]; } if ( $data['count'] >= $limit ) { // Tell the client exactly how many seconds remain in this window. $retry_after = $window_size - ( $now - $data['start'] ); return new WP_REST_Response( [ 'code' => '100prints_rate_limit', 'message' => 'Too many requests. Please wait before trying again.', ], 429, [ 'Retry-After' => (string) max( 1, $retry_after ) ] ); } // Increment the counter and save with a TTL that expires at window end, // not at "now + 60s", so repeated hits cannot extend the block. $data['count']++; $remaining_ttl = $window_size - ( $now - $data['start'] ); set_transient( $transient_key, $data, max( 1, $remaining_ttl ) ); return true; } /** * Log a generation event. */ public function log_generation( $request ) { $params = $request->get_json_params(); if ( empty( $params ) ) { $params = json_decode( $request->get_body(), true ); } if ( ! is_array( $params ) ) { $params = []; } $settings = hundred_prints_get_settings(); if ( isset( $settings['log_enabled'] ) && ! $settings['log_enabled'] ) { return rest_ensure_response( [ 'success' => true, 'logged' => false ] ); } $template_id = isset( $params['templateId'] ) ? hundred_prints_sanitize_template_id( $params['templateId'] ) : ''; if ( empty( $template_id ) ) { return new WP_Error( '100prints_bad_request', 'Missing template ID', [ 'status' => 400 ] ); } $visitor_ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : ''; $args = [ 'template_id' => $template_id, 'template_name' => isset( $params['templateName'] ) ? sanitize_text_field( $params['templateName'] ) : '', 'page_id' => isset( $params['pageId'] ) ? absint( $params['pageId'] ) : 0, 'format' => isset( $params['format'] ) ? sanitize_key( $params['format'] ) : 'pdf', 'status' => isset( $params['status'] ) ? sanitize_key( $params['status'] ) : 'success', 'quality' => isset( $params['quality'] ) ? sanitize_key( $params['quality'] ) : 'high', 'variables' => isset( $params['variables'] ) && is_array( $params['variables'] ) ? $params['variables'] : [], 'visitor_ip' => $visitor_ip, ]; $analytics_service = new Hundred_Prints_Analytics_Service(); $logged = $analytics_service->log_generation( $args ); return rest_ensure_response( [ 'success' => true, 'logged' => (bool) $logged ] ); } }