PluginProbe
WP API SwaggerUI / trunk
WP API SwaggerUI vtrunk
2.4.0 2.3.0 2.2.0 2.1.0 2.1.1 2.0.3 2.0.1 2.0.2 trunk 1.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.2.0 2.0.0
wp-api-swaggerui / wp-api-swaggerui.php

wp-api-swaggerui.php in WP API SwaggerUI trunk, at wp-api-swaggerui.php

857 lines 31.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WP API SwaggerUI
4 *
5 * @package WP API SwaggerUI
6 * @author Agus Suroyo
7 * @copyright 2019 Agus Suroyo
8 * @license GPL-2.0-or-later
9 *
10 * @wordpress-plugin
11 * Plugin Name: WP API SwaggerUI
12 * Description: WordPress REST API with Swagger UI.
13 * Version: 2.4.0
14 * Author: Agus Suroyo
15 * Requires PHP: 7.4
16 * License: GPL v2 or later
17 * License URI: http://www.gnu.org/licenses/gpl-2.0.txt
18 */
19 global $wp_version;
20
21 if (version_compare(PHP_VERSION, '7.4', '<') || version_compare($wp_version, '4.7', '<')) {
22 return;
23 }
24
25 require_once __DIR__ . DIRECTORY_SEPARATOR . 'swaggerbag.php';
26 require_once __DIR__ . DIRECTORY_SEPARATOR . 'swaggerauth.php';
27 require_once __DIR__ . DIRECTORY_SEPARATOR . 'swaggertemplate.php';
28 require_once __DIR__ . DIRECTORY_SEPARATOR . 'swaggeropenapi.php';
29
30 if (is_admin()) {
31 require_once __DIR__ . DIRECTORY_SEPARATOR . 'swaggersetting.php';
32 }
33
34 class WP_API_SwaggerUI
35 {
36
37 public function routes()
38 {
39 $base = self::rewriteBaseApi();
40 add_rewrite_tag('%swagger_api%', '([^&]+)');
41 add_rewrite_rule('^' . $base . '/docs/?', 'index.php?swagger_api=docs', 'top');
42 add_rewrite_rule('^' . $base . '/schema/?', 'index.php?swagger_api=schema', 'top');
43 }
44
45 public static function rewriteBaseApi()
46 {
47 return apply_filters('swagger_api_rewrite_api_base', 'rest-api');
48 }
49
50 public static function pluginUrl($path = null)
51 {
52 return plugin_dir_url(__FILE__) . $path;
53 }
54
55 public static function endpointUrl($endpoint)
56 {
57 return add_query_arg('swagger_api', $endpoint, home_url('/'));
58 }
59
60 public static function pluginPath($path)
61 {
62 return plugin_dir_path(__FILE__) . $path;
63 }
64
65 public function swagger()
66 {
67 if (get_query_var('swagger_api') !== 'schema') {
68 return;
69 }
70
71 global $wp_version;
72
73 $expose_email = '1' === get_option('swagger_api_expose_contact_email', '1');
74 $contact_email = apply_filters('swagger_api_contact_email', $expose_email ? get_option('admin_email') : '');
75
76 $info = array(
77 'title' => get_option('blogname') . ' API',
78 'description' => get_option('blogdescription'),
79 'version' => apply_filters('swagger_api_info_version', $wp_version),
80 );
81 if (!empty($contact_email)) {
82 $info['contact'] = array('email' => $contact_email);
83 }
84
85 // Canonical Swagger 2.0 pivot document. Each formatter stamps its own
86 // version marker (swagger/openapi) and reshapes from here.
87 $response = array(
88 'info' => $info,
89 'host' => $this->getHost(),
90 'basePath' => $this->getBasePath(),
91 'tags' => [],
92 'schemes' => $this->getSchemes(),
93 'paths' => $this->getPaths(),
94 );
95
96 // Only advertise securityDefinitions when a scheme is enabled. Omitting
97 // the key (rather than emitting an empty map) keeps /schema valid Swagger
98 // 2.0 and stops Swagger UI rendering a stray, empty Authorize dialog.
99 $securityDefinitions = $this->securityDefinitions();
100 if (is_array($securityDefinitions) && !empty($securityDefinitions)) {
101 $response['securityDefinitions'] = $securityDefinitions;
102 }
103
104 $formatter = SwaggerSpecRegistry::forVersion(get_option('swagger_api_spec_version', '2.0'));
105 $output = $formatter->format($response);
106 if (empty($output['paths'])) {
107 $output['paths'] = new \stdClass();
108 }
109 wp_send_json($output);
110 }
111
112 public function getHost()
113 {
114 $host = parse_url(home_url(), PHP_URL_HOST);
115 $port = parse_url(home_url(), PHP_URL_PORT);
116
117 if ($port) {
118 if ($port != 80 && $port != 443) {
119 $host = $host . ':' . $port;
120 }
121 }
122
123 return $host;
124 }
125
126 public function getBasePath()
127 {
128 $path = parse_url(home_url(), PHP_URL_PATH) ?? '';
129 return rtrim($path, '/') . '/' . ltrim(rest_get_url_prefix(), '/');
130 }
131
132 // Client-side data for rewriting Swagger UI Try-it-out requests to the
133 // ?rest_route= form when permalinks are Plain. rest_url() has no pretty
134 // /wp-json route then, so Swagger UI's server+path URLs 404; the JS
135 // requestInterceptor uses this to rebuild each REST call.
136 //
137 // strip = the URL path Swagger UI prepends for the active spec version.
138 // OpenAPI 3.0 can advertise an honest ?rest_route= server (returned in
139 // 'server'); Swagger 2.0's host+basePath cannot carry a query string, so
140 // it keeps the /wp-json base and only the interceptor makes it work.
141 public static function restRouteConfig()
142 {
143 $rest_root = explode('?', rest_url('/'))[0];
144 $self = new self();
145
146 if ('3.0.3' === get_option('swagger_api_spec_version', '2.0')) {
147 $server = $rest_root . '?rest_route=';
148 $strip = parse_url($rest_root, PHP_URL_PATH);
149 } else {
150 $server = null;
151 $strip = $self->getBasePath();
152 }
153
154 return array(
155 'enabled' => ! get_option('permalink_structure'),
156 'restRoot' => $rest_root,
157 'strip' => $strip,
158 'server' => $server,
159 );
160 }
161
162 public function getSchemes()
163 {
164 $schemes = [];
165 if (is_ssl()) {
166 $schemes[] = 'https';
167 }
168 $schemes[] = 'http';
169 return $schemes;
170 }
171
172 public static function getNameSpace()
173 {
174 return '/' . trim(get_option('swagger_api_basepath', '/wp/v2'), '/');
175 }
176
177 public static function getCLeanNameSpace()
178 {
179 return trim(self::getNameSpace(), '/');
180 }
181
182 public function getRawPaths()
183 {
184 $routes = rest_get_server()->get_routes();
185 $basepath = self::getNameSpace();
186
187 $raw_paths = [];
188 foreach ($routes as $route => $value) {
189 if (mb_strpos($route, $basepath) === 0 && ($basepath !== $route)) {
190 $raw_paths[$route] = $value;
191 }
192 }
193
194 return $raw_paths;
195 }
196
197 public function getPaths()
198 {
199 $raw = $this->getRawPaths();
200
201 $paths = [];
202
203 foreach ($raw as $endpoint => $args) {
204 $ep = $this->convertEndpoint($endpoint);
205 $paths[$ep] = $this->getMethodsFromArgs($ep, $endpoint, $args);
206 }
207
208 return $paths;
209 }
210
211 public function convertEndpoint($endpoint)
212 {
213
214 if (mb_strpos($endpoint, '(?P<') !== false) {
215 // Match each named group separately (multi-param routes). Atoms:
216 // escaped char, char class [...], or a (?3)-recursive balanced group,
217 // so a param regex may nest parens to any depth and may contain a
218 // literal ')' inside a class or escape without ending the group early.
219 $endpoint = preg_replace_callback('/\(\?P<([^>]+)>((?:\\\\.|\[[^\]]*\]|(\((?:\\\\.|\[[^\]]*\]|[^()]|(?3))*\))|[^()])*)\)/', function ($match) {
220 return '{' . $match[1] . '}';
221 }, $endpoint);
222 }
223
224 return $endpoint;
225 }
226
227 public function getDefaultTagsFromEndpoint($endpoint)
228 {
229 $namespace = self::getNameSpace();
230 $ep = preg_replace_callback('/^' . preg_quote($namespace, '/') . '/', function () {
231 return '';
232 }, $endpoint);
233 // Strip named parameters so the tag comes from a real path segment,
234 // not a raw (?P<...>) regex. Same recursive match as convertEndpoint.
235 $ep = preg_replace_callback('/\(\?P<([^>]+)>((?:\\\\.|\[[^\]]*\]|(\((?:\\\\.|\[[^\]]*\]|[^()]|(?3))*\))|[^()])*)\)/', function () {
236 return '';
237 }, $ep);
238 $parts = array_values(array_filter(explode('/', trim($ep, '/'))));
239 return isset($parts[0]) ? [$parts[0]] : [];
240 }
241
242 public function getMethodsFromArgs($ep, $endpoint, $args)
243 {
244
245 $path_parameters = $this->getParametersFromEndpoint($endpoint);
246 $methods = [];
247
248 $tags = $this->getDefaultTagsFromEndpoint($endpoint);
249
250 foreach ($args as $arg) {
251
252 $all_parameters = $this->getParametersFromArgs(
253 $ep,
254 isset($arg['args']) ? $arg['args'] : [],
255 isset($arg['methods']) ? $arg['methods'] : []
256 );
257
258 foreach ($arg['methods'] as $method => $bool) {
259 $mtd = mb_strtolower($method);
260 $methodEndpoint = $mtd . str_replace('/', '_', $ep);
261 $parameters = isset($all_parameters[$mtd]) ? $all_parameters[$mtd] : [];
262
263 // Building parameters.
264 $existing_names = array_map(function ($param) {
265 return $param['name'];
266 }, $parameters);
267 foreach ($path_parameters as $path_params) {
268 if (!in_array($path_params['name'], $existing_names, true)) {
269 $parameters[] = $path_params;
270 }
271 }
272
273 $produces = ['application/json'];
274 if (isset($arg['produces'])) {
275 $produces = (array) $arg['produces'];
276 }
277
278 $consumes = [
279 'application/x-www-form-urlencoded',
280 'multipart/form-data',
281 ];
282
283 if (isset($arg['consumes'])) {
284 $consumes = (array) $arg['consumes'];
285 }
286
287 if ($arg['accept_json']) {
288 $consumes[] = 'application/json';
289 }
290
291 $has_file = false;
292 $has_explicit_body = false;
293 foreach ($parameters as $parameter) {
294 if ($this->schemaContainsFile($parameter)) {
295 $has_file = true;
296 }
297 if (isset($parameter['in']) && 'body' === $parameter['in']) {
298 $has_explicit_body = true;
299 }
300 }
301 $wants_json = in_array('application/json', $consumes, true);
302
303 if ($has_file) {
304 // Swagger 2 file parameters are valid only under multipart/form-data, and a
305 // body parameter cannot coexist with formData. Flatten an explicit object body
306 // into individual multipart fields and drop the body parameter.
307 $consumes = ['multipart/form-data'];
308 $flattened = array();
309 foreach ($parameters as $parameter) {
310 if (isset($parameter['in']) && 'body' === $parameter['in']) {
311 $schema = isset($parameter['schema']) && is_array($parameter['schema']) ? $parameter['schema'] : array();
312 $props = isset($schema['properties']) && is_array($schema['properties']) ? $schema['properties'] : array();
313 $body_required = isset($schema['required']) && is_array($schema['required']) ? $schema['required'] : array();
314 foreach ($props as $name => $property) {
315 $field = is_array($property) ? $property : array();
316 $field['name'] = $name;
317 $field['in'] = 'formData';
318 $field['required'] = in_array($name, $body_required, true);
319 $flattened[] = $this->normalizeNonBodyParameter($field);
320 }
321 continue;
322 }
323 $flattened[] = $this->normalizeNonBodyParameter($parameter);
324 }
325 $parameters = $flattened;
326 } elseif ($wants_json || $has_explicit_body) {
327 // Consolidate form fields into the single body parameter Swagger 2 allows.
328 $parameters = $this->buildJsonBodyParameter($parameters);
329 $has_body = false;
330 foreach ($parameters as $index => $parameter) {
331 if (isset($parameter['in']) && 'body' === $parameter['in']) {
332 $has_body = true;
333 } else {
334 // Query/header params still cannot carry object schemas in Swagger 2.
335 $parameters[$index] = $this->normalizeNonBodyParameter($parameter);
336 }
337 }
338 // A body parameter cannot coexist with form media types. Drop those, but keep
339 // any declared body-compatible type (e.g. XML); only default to JSON if none remain.
340 if ($has_body) {
341 $consumes = array_values(array_diff($consumes, ['application/x-www-form-urlencoded', 'multipart/form-data']));
342 if (empty($consumes)) {
343 $consumes = ['application/json'];
344 }
345 }
346 } else {
347 $parameters = array_map([$this, 'normalizeNonBodyParameter'], $parameters);
348 }
349
350 $responses =$this->getResponses($methodEndpoint);
351 if (isset($arg['responses'])) {
352 $responses = $arg['responses'];
353 }
354
355 $conf = array(
356 'tags' => isset($arg['tags']) ? (array) $arg['tags'] : $tags,
357 'summary' => isset($arg['summary']) ? $arg['summary'] : '',
358 'description' => isset($arg['description']) ? $arg['description'] : '',
359 'consumes' => $consumes,
360 'produces' => $produces,
361 'parameters' => $parameters,
362 'security' => $this->getSecurity(),
363 'responses' => $responses
364 );
365
366 $methods[$mtd] = $conf;
367 }
368 }
369
370 return $methods;
371 }
372
373 public function getParametersFromEndpoint($endpoint)
374 {
375 $path_params = [];
376
377 if (mb_strpos($endpoint, '(?P<') !== false && (preg_match_all('/\(\?P<([^>]+)>((?:\\\\.|\[[^\]]*\]|(\((?:\\\\.|\[[^\]]*\]|[^()]|(?3))*\))|[^()])*)\)/', $endpoint, $matches))) {
378 foreach ($matches[1] as $order => $match) {
379 $type = strpos(mb_strtolower($matches[2][$order]), '\d') !== false ? 'integer' : 'string';
380 $params = array(
381 'name' => $match,
382 'in' => 'path',
383 'description' => '',
384 'required' => true,
385 'type' => $type,
386 );
387 if ($type === 'integer') {
388 $params['format'] = 'int64';
389 }
390 $path_params[$match] = $params;
391 }
392 }
393
394 return $path_params;
395 }
396
397 public function detectIn($param, $mtd, $endpoint, $detail)
398 {
399 if (isset($detail['in'])) {
400 return $detail['in'];
401 }
402
403 switch ($mtd) {
404 case strpos($endpoint, '{' . $param . '}') !== false:
405 $in = 'path';
406 break;
407 case 'post':
408 case 'put':
409 case 'patch':
410 $in = 'formData';
411 break;
412 default:
413 $in = 'query';
414 break;
415 }
416
417 return $in;
418 }
419
420 public function buildParams($param, $mtd, $endpoint, $detail)
421 {
422 if (!is_array($detail)) {
423 $detail = array();
424 }
425 $schema = $this->normalizeSchema($detail);
426 // Null when normalizeSchema intentionally omits type ($ref or composition-only);
427 // forcing 'string' here would contradict the $ref/allOf once folded into a body.
428 $type = isset($schema['type']) ? $schema['type'] : null;
429
430 $in = $this->detectIn($param, $mtd, $endpoint, $detail);
431 // A JSON-Schema `required` array is the object's schema-level list, not field requiredness.
432 $required = !empty($detail['required']) && !is_array($detail['required']);
433
434 // Swagger 2 body parameters carry their schema under `schema`, not primitive fields.
435 if ('body' === $in) {
436 return array(
437 'name' => $param,
438 'in' => 'body',
439 'description' => isset($detail['description']) ? $detail['description'] : '',
440 'required' => $required,
441 'schema' => isset($detail['schema']) ? $this->normalizeSchema($detail['schema']) : $schema,
442 );
443 }
444
445 // Typeless `id` / `*_id` arguments are conventionally integers.
446 if (!isset($detail['type']) && 'string' === $type
447 && ('id' === strtolower($param) || false !== strpos($param, '_id'))) {
448 $type = 'integer';
449 }
450
451 if ('path' === $in) {
452 $required = true;
453 }
454
455 $params = array(
456 'name' => $param,
457 'in' => $in,
458 'description' => isset($detail['description']) ? $detail['description'] : '',
459 'required' => $required,
460 );
461 if (null !== $type) {
462 $params['type'] = $type;
463 }
464
465 foreach ($schema as $key => $value) {
466 if ('description' !== $key && 'required' !== $key && 'type' !== $key) {
467 $params[$key] = $value;
468 }
469 }
470
471 // Object sub-property requirements are schema-level; carry them separately so the
472 // param's own boolean requiredness ($params['required']) is preserved.
473 if ('object' === $type && isset($schema['required'])) {
474 $params['objectRequired'] = $schema['required'];
475 }
476
477 if ('array' === $type && isset($detail['enum']) && !isset($detail['items'])) {
478 $params['collectionFormat'] = 'multi';
479 }
480
481 if ('integer' === $type && !isset($params['format'])) {
482 $params['format'] = 'int64';
483 }
484
485 return $params;
486 }
487
488 /**
489 * @deprecated Use normalizeSchema() instead. Retained for backward compatibility.
490 */
491 public function parseTypeObjectToString($types)
492 {
493 if (is_array($types)) {
494 foreach ($types as $type) {
495 return $this->parseTypeObjectToString($type);
496 }
497 }
498 return 'object' === $types ? 'string' : $types;
499 }
500
501 /** Normalize a WordPress REST argument into a recursively complete schema. */
502 public function normalizeSchema($detail)
503 {
504 if (!is_array($detail)) {
505 return array('type' => 'string');
506 }
507
508 // A $ref replaces the schema; it carries no sibling keywords.
509 if (isset($detail['$ref'])) {
510 return array('$ref' => $detail['$ref']);
511 }
512
513 $schema = array();
514 $type = null;
515 if (isset($detail['type'])) {
516 $type = $detail['type'];
517 if (is_array($type)) {
518 // Union types: drop the non-representable "null" and keep the first real type.
519 $type = array_values(array_filter($type, function ($member) {
520 return 'null' !== $member;
521 }));
522 $type = !empty($type) ? reset($type) : 'string';
523 }
524 // Some routes use the non-standard type "enum". Keep their intent valid.
525 $type = ('enum' === $type) ? 'string' : ($type ?: 'string');
526 } elseif (isset($detail['properties'])) {
527 $type = 'object';
528 } elseif (isset($detail['items'])) {
529 $type = 'array';
530 } elseif (isset($detail['allOf'])) {
531 $type = null; // composition-only schema has no primitive type
532 } elseif ((isset($detail['required']) && is_array($detail['required'])) || isset($detail['additionalProperties']) || isset($detail['minProperties']) || isset($detail['maxProperties'])) {
533 $type = 'object'; // object-only keywords imply an object
534 } else {
535 $type = 'string';
536 }
537 if (null !== $type) {
538 $schema['type'] = $type;
539 }
540
541 // Keep only keywords valid in Swagger 2.0 (the base spec this plugin emits).
542 // oneOf/anyOf/const/patternProperties are not; dropping beats emitting invalid output.
543 $keys = array('description', 'format', 'enum', 'default', 'example', 'minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf', 'minLength', 'maxLength', 'pattern', 'minItems', 'maxItems', 'uniqueItems', 'minProperties', 'maxProperties', 'title', 'xml');
544 foreach ($keys as $key) {
545 if (array_key_exists($key, $detail)) {
546 $schema[$key] = $detail[$key];
547 }
548 }
549
550 if (isset($detail['additionalProperties'])) {
551 $additional = $detail['additionalProperties'];
552 if (is_array($additional)) {
553 // Empty schema {} means "any value"; keep it open rather than narrowing to string.
554 $schema['additionalProperties'] = empty($additional) ? true : $this->normalizeSchema($additional);
555 } else {
556 $schema['additionalProperties'] = $additional;
557 }
558 }
559 if (isset($detail['allOf']) && is_array($detail['allOf'])) {
560 $schema['allOf'] = array_map(array($this, 'normalizeSchema'), $detail['allOf']);
561 }
562
563 if (isset($detail['items'])) {
564 $schema['items'] = $this->normalizeSchema($detail['items']);
565 } elseif ('array' === $type) {
566 $items = array('type' => 'string');
567 if (isset($schema['enum'])) {
568 $items['enum'] = $schema['enum'];
569 unset($schema['enum']);
570 // The scalar default constrains items, not the array itself.
571 if (isset($schema['default']) && !is_array($schema['default'])) {
572 $items['default'] = $schema['default'];
573 unset($schema['default']);
574 }
575 }
576 $schema['items'] = $items;
577 }
578
579 // Object-level required (JSON Schema array) applies even when properties arrive via allOf.
580 $required = (isset($detail['required']) && is_array($detail['required'])) ? $detail['required'] : array();
581 if (isset($detail['properties']) && is_array($detail['properties'])) {
582 $schema['properties'] = array();
583 foreach ($detail['properties'] as $name => $property) {
584 $schema['properties'][$name] = $this->normalizeSchema($property);
585 if (is_array($property) && !empty($property['required']) && !is_array($property['required'])) {
586 $required[] = $name;
587 }
588 }
589 }
590 $required = array_values(array_unique($required));
591 if (!empty($required)) {
592 $schema['required'] = $required;
593 }
594
595 return $schema;
596 }
597
598 /** Combine inferred form fields into the one body parameter Swagger 2 supports. */
599 public function buildJsonBodyParameter($parameters)
600 {
601 $ordinary = array();
602 $properties = array();
603 $required = array();
604
605 foreach ($parameters as $parameter) {
606 if (!isset($parameter['in']) || 'formData' !== $parameter['in']) {
607 $ordinary[] = $parameter;
608 continue;
609 }
610 $name = $parameter['name'];
611 $field_required = !empty($parameter['required']);
612 $schema = $parameter;
613 foreach (array('name', 'in', 'required', 'collectionFormat', 'objectRequired') as $key) {
614 unset($schema[$key]);
615 }
616 if (isset($parameter['objectRequired'])) {
617 $schema['required'] = $parameter['objectRequired'];
618 }
619 $properties[$name] = $schema;
620 if ($field_required) {
621 $required[] = $name;
622 }
623 }
624
625 // Swagger 2 permits a single body parameter: collapse multiple explicit ones into the first.
626 $bodyIndex = null;
627 foreach ($ordinary as $index => $parameter) {
628 if (!isset($parameter['in']) || 'body' !== $parameter['in']) {
629 continue;
630 }
631 if (null === $bodyIndex) {
632 $bodyIndex = $index;
633 continue;
634 }
635 if ($this->bodySchemaIsObjectCompatible($this->bodySchema($ordinary[$bodyIndex]))
636 && $this->bodySchemaIsObjectCompatible($this->bodySchema($parameter))) {
637 $ordinary[$bodyIndex]['schema'] = $this->mergeObjectSchema($this->bodySchema($ordinary[$bodyIndex]), $this->bodySchema($parameter));
638 if (!empty($parameter['required'])) {
639 $ordinary[$bodyIndex]['required'] = true;
640 }
641 }
642 unset($ordinary[$index]);
643 }
644 $ordinary = array_values($ordinary);
645
646 if (empty($properties)) {
647 return $ordinary;
648 }
649
650 // Merge inferred form fields into an existing body when it is object-compatible.
651 foreach ($ordinary as $index => $parameter) {
652 if (!isset($parameter['in']) || 'body' !== $parameter['in']) {
653 continue;
654 }
655 if (!$this->bodySchemaIsObjectCompatible($this->bodySchema($parameter))) {
656 return $ordinary; // cannot fold form fields into a non-object body
657 }
658 $ordinary[$index]['schema'] = $this->mergeObjectSchema($this->bodySchema($parameter), array('properties' => $properties, 'required' => $required));
659 if (!empty($required)) {
660 $ordinary[$index]['required'] = true;
661 }
662 return $ordinary;
663 }
664
665 $schema = array('type' => 'object', 'properties' => $properties);
666 if (!empty($required)) {
667 $schema['required'] = $required;
668 }
669 $ordinary[] = array(
670 'name' => 'body',
671 'in' => 'body',
672 'description' => '',
673 'required' => !empty($required),
674 'schema' => $schema,
675 );
676
677 return $ordinary;
678 }
679
680 private function bodySchema($parameter)
681 {
682 return isset($parameter['schema']) && is_array($parameter['schema']) ? $parameter['schema'] : array();
683 }
684
685 /** An object body can absorb inferred form fields; a $ref, allOf, or scalar/array body cannot. */
686 private function bodySchemaIsObjectCompatible($schema)
687 {
688 if (isset($schema['$ref']) || isset($schema['allOf'])) {
689 return false;
690 }
691 return !isset($schema['type']) || 'object' === $schema['type'];
692 }
693
694 /** Merge one object body schema's properties and required list into another. */
695 private function mergeObjectSchema($target, $source)
696 {
697 $target['type'] = 'object';
698 if (!isset($target['properties']) || !is_array($target['properties'])) {
699 $target['properties'] = array();
700 }
701 if (isset($source['properties']) && is_array($source['properties'])) {
702 $target['properties'] += $source['properties'];
703 }
704 $required = isset($target['required']) && is_array($target['required']) ? $target['required'] : array();
705 if (isset($source['required']) && is_array($source['required'])) {
706 $required = array_merge($required, $source['required']);
707 }
708 if (!empty($required)) {
709 $target['required'] = array_values(array_unique($required));
710 }
711 return $target;
712 }
713
714 /** Swagger 2 query/form parameters cannot carry object or reference schemas at any depth. */
715 public function normalizeNonBodyParameter($parameter)
716 {
717 if ($this->schemaIsStructured($parameter)) {
718 $parameter['type'] = 'string';
719 foreach (array('properties', 'objectRequired', 'additionalProperties', 'allOf', '$ref', 'items', 'schema', 'collectionFormat', 'minProperties', 'maxProperties', 'minItems', 'maxItems', 'uniqueItems') as $key) {
720 unset($parameter[$key]);
721 }
722 }
723 // Swagger 2 Parameter Objects (non-body) permit only a fixed key subset; schema-only
724 // metadata such as title/example/xml is invalid here and must not leak through.
725 $allowed = array('name', 'in', 'description', 'required', 'type', 'format', 'allowEmptyValue', 'items', 'collectionFormat', 'default', 'maximum', 'exclusiveMaximum', 'minimum', 'exclusiveMinimum', 'maxLength', 'minLength', 'pattern', 'maxItems', 'minItems', 'uniqueItems', 'enum', 'multipleOf');
726 $parameter = array_intersect_key($parameter, array_flip($allowed));
727 // A string parameter cannot carry an array default left over from the downgrade.
728 if (isset($parameter['type']) && 'string' === $parameter['type'] && isset($parameter['default']) && is_array($parameter['default'])) {
729 unset($parameter['default']);
730 }
731 return $parameter;
732 }
733
734 /** A query/form schema is unrepresentable in Swagger 2 if it nests an object or $ref. */
735 private function schemaIsStructured($schema)
736 {
737 if (!is_array($schema)) {
738 return false;
739 }
740 if (isset($schema['$ref']) || isset($schema['properties']) || isset($schema['additionalProperties']) || isset($schema['allOf'])) {
741 return true;
742 }
743 if (isset($schema['type']) && 'object' === $schema['type']) {
744 return true;
745 }
746 if (isset($schema['schema']) && $this->schemaIsStructured($schema['schema'])) {
747 return true;
748 }
749 if (isset($schema['items'])) {
750 return $this->schemaIsStructured($schema['items']);
751 }
752 return false;
753 }
754
755 /** Detect a Swagger 2 `file` type anywhere in a built parameter tree. */
756 private function schemaContainsFile($schema)
757 {
758 if (!is_array($schema)) {
759 return false;
760 }
761 if (isset($schema['type']) && 'file' === $schema['type']) {
762 return true;
763 }
764 foreach (array('items', 'schema', 'additionalProperties') as $key) {
765 if (isset($schema[$key]) && $this->schemaContainsFile($schema[$key])) {
766 return true;
767 }
768 }
769 foreach (array('properties', 'allOf') as $key) {
770 if (isset($schema[$key]) && is_array($schema[$key])) {
771 foreach ($schema[$key] as $sub) {
772 if ($this->schemaContainsFile($sub)) {
773 return true;
774 }
775 }
776 }
777 }
778 return false;
779 }
780
781 public function getParametersFromArgs($endpoint = '', $args = [], $methods = [])
782 {
783 $parameters = [];
784
785 foreach ($args as $param => $detail) {
786 foreach ($methods as $method => $bool) {
787 $mtd = mb_strtolower($method);
788
789 if (!isset($parameters[$mtd])) {
790 $parameters[$mtd] = [];
791 }
792
793 $parameters[$mtd][] = $this->buildParams($param, $mtd, $endpoint, $detail);
794 }
795 }
796
797 return $parameters;
798 }
799
800 public function getSecurity()
801 {
802 $raw = $this->securityDefinitions();
803 if (!is_array($raw)) {
804 $raw = [];
805 }
806
807 $securities = [];
808 foreach ($raw as $key => $name) {
809 $securities[] = array(
810 $key => []
811 );
812 }
813
814 return $securities;
815 }
816
817 public function getResponses( $methodEndpoint ) {
818 return apply_filters('swagger_api_responses_' . $methodEndpoint, array(
819 '200' => ['description' => 'OK'],
820 '404' => ['description' => 'Not Found'],
821 '400' => ['description' => 'Bad Request']
822 ));
823 }
824
825 public function securityDefinitions()
826 {
827 return apply_filters('swagger_api_security_definitions', null);
828 }
829
830 public function flushActivate()
831 {
832 $this->routes();
833 flush_rewrite_rules();
834 }
835
836 public function flushDeactivate()
837 {
838 flush_rewrite_rules();
839 }
840
841 public static function debug($params = null)
842 {
843 echo '<pre>';
844 print_r($params);
845 echo '</pre>';
846 die();
847 }
848
849 }
850
851 $swagerui = new WP_API_SwaggerUI();
852
853 register_activation_hook(__FILE__, [$swagerui, 'flushActivate']);
854 register_deactivation_hook(__FILE__, [$swagerui, 'flushDeactivate']);
855 add_action('init', [$swagerui, 'routes']);
856 add_action('wp', [$swagerui, 'swagger']);
857