| @@ -36,8 +36,26 @@ | ||
| 36 | 36 | class Image_SEO_Endpoint extends WP_REST_Controller { |
| 37 | 37 | use CSRF_Protection; |
| 38 | 38 | |
| 39 | 39 | /** |
| 40 | + * Sanitizer per schema type, for settings args built from the schema. | |
| 41 | + * | |
| 42 | + * Only scalar types appear here. An array or object type deliberately gets | |
| 43 | + * no `sanitize_callback` — core falls back to `rest_parse_request_arg`, | |
| 44 | + * which sanitizes against the declared schema, where a string sanitizer | |
| 45 | + * would flatten the value to "Array" or "". | |
| 46 | + * | |
| 47 | + * @since 2.0.1 | |
| 48 | + * @var array<string, string> | |
| 49 | + */ | |
| 50 | + private const SANITIZERS = [ | |
| 51 | + 'boolean' => 'rest_sanitize_boolean', | |
| 52 | + 'string' => 'sanitize_text_field', | |
| 53 | + 'integer' => 'absint', | |
| 54 | + 'number' => 'floatval', | |
| 55 | + ]; | |
| 56 | + | |
| 57 | + /** | |
| 40 | 58 | * Image SEO Manager instance |
| 41 | 59 | * |
| 42 | 60 | * @since 1.0.0 |
| 43 | 61 | * @var Image_SEO_Manager |
| @@ -129,8 +147,13 @@ | ||
| 129 | 147 | 'limit' => [ |
| 130 | 148 | 'type' => 'integer', |
| 131 | 149 | 'required' => false, |
| 132 | 150 | 'default' => 50, |
| 151 | + // Bounded: ?limit=100000 walked the whole media | |
| 152 | + // library synchronously, and with alt_source=ai | |
| 153 | + // that is one AI call per image (#394). | |
| 154 | + 'minimum' => 1, | |
| 155 | + 'maximum' => 500, | |
| 133 | 156 | 'sanitize_callback' => 'absint' |
| 134 | 157 | ], |
| 135 | 158 | 'overwrite' => [ |
| 136 | 159 | 'type' => 'boolean', |
| @@ -235,10 +258,30 @@ | ||
| 235 | 258 | foreach ($schema as $key => $config) { |
| 236 | 259 | $args[$key] = [ |
| 237 | 260 | 'type' => $config['type'], |
| 238 | 261 | 'required' => false, |
| 239 | - 'sanitize_callback' => $config['type'] === 'boolean' ? 'rest_sanitize_boolean' : 'sanitize_text_field' | |
| 240 | 262 | ]; |
| 263 | + | |
| 264 | + // Pick the sanitizer from the declared type. `sanitize_text_field` | |
| 265 | + // for everything non-boolean was a trap for the first array- or | |
| 266 | + // object-typed setting added to the schema: it casts an array to | |
| 267 | + // the string "Array" (PHP notice) or an empty string, so the value | |
| 268 | + // would arrive at the handler destroyed rather than rejected. | |
| 269 | + // A type with no scalar sanitizer gets none — core then falls back | |
| 270 | + // to `rest_parse_request_arg`, which sanitizes against this very | |
| 271 | + // schema instead of flattening it. | |
| 272 | + $sanitizer = self::SANITIZERS[$config['type']] ?? null; | |
| 273 | + | |
| 274 | + if (null !== $sanitizer) { | |
| 275 | + $args[$key]['sanitize_callback'] = $sanitizer; | |
| 276 | + } | |
| 277 | + | |
| 278 | + // A structural type is unusable to core without its shape. | |
| 279 | + foreach (['items', 'properties', 'additionalProperties'] as $keyword) { | |
| 280 | + if (isset($config[$keyword])) { | |
| 281 | + $args[$key][$keyword] = $config[$keyword]; | |
| 282 | + } | |
| 283 | + } | |
| 241 | 284 | |
| 242 | 285 | // Carry through any constraint the schema already declares. Copying |
| 243 | 286 | // only type/required/sanitize_callback silently dropped the |
| 244 | 287 | // alt_source enum, so the REST validator never enforced it. |