This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | declare(strict_types=1); |
||
3 | |||
4 | namespace hiapi\Service\OpenApi; |
||
5 | |||
6 | use cebe\openapi\spec\Components; |
||
7 | use cebe\openapi\spec\OpenApi; |
||
8 | use cebe\openapi\spec\Operation; |
||
9 | use cebe\openapi\spec\PathItem; |
||
10 | use cebe\openapi\spec\Response; |
||
11 | use cebe\openapi\spec\Responses; |
||
12 | use cebe\openapi\spec\Schema; |
||
13 | use cebe\openapi\spec\SecurityRequirement; |
||
14 | use cebe\openapi\spec\SecurityScheme; |
||
15 | use cebe\openapi\spec\Server; |
||
16 | use cebe\openapi\spec\ServerVariable; |
||
17 | use Generator; |
||
18 | use hiapi\commands\Reflection\BaseCommandReflection; |
||
19 | use hiapi\Core\Endpoint\EndpointRepository; |
||
20 | use hiapi\endpoints\Module\InOutControl\VO\Collection; |
||
21 | use hiapi\exceptions\ConfigurationException; |
||
22 | use hiapi\validators\Reflection\ValidatorReflection; |
||
23 | use yii\validators\EachValidator; |
||
24 | use yii\validators\InlineValidator; |
||
25 | use yii\validators\RequiredValidator; |
||
26 | use yii\validators\SafeValidator; |
||
27 | use yii\validators\Validator; |
||
28 | use ReflectionObject; |
||
29 | |||
30 | /** |
||
31 | * Class OpenAPIGenerator generates OpenAPI documentation. |
||
32 | * |
||
33 | * This class represents a general implementation. |
||
34 | * |
||
35 | * @author Dmytro Naumenko <[email protected]> |
||
36 | */ |
||
37 | class OpenAPIGenerator |
||
38 | { |
||
39 | /** @psalm-var array<string, SecurityScheme> */ |
||
40 | private array $securitySchemes; |
||
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||
41 | private EndpointRepository $endpointRepository; |
||
42 | /** @psalm-var list<string> */ |
||
43 | private array $hosts; |
||
44 | private array $apiInfo; |
||
45 | |||
46 | public function __construct(EndpointRepository $endpointRepository, array $options = []) |
||
47 | { |
||
48 | $this->endpointRepository = $endpointRepository; |
||
49 | |||
50 | $this->hosts = $options['hosts'] ?? []; |
||
51 | $this->securitySchemes = $options['securitySchemes'] ?? []; |
||
52 | $this->apiInfo = $options['apiInfo'] ?? []; |
||
53 | } |
||
54 | |||
55 | public function __invoke(): OpenApi |
||
56 | { |
||
57 | return $this->createOpenAPI(); |
||
58 | } |
||
59 | |||
60 | private function createOpenAPI(): OpenApi |
||
61 | { |
||
62 | return new OpenApi([ |
||
63 | 'openapi' => '3.0.2', |
||
64 | 'info' => $this->apiInfo, |
||
65 | 'paths' => iterator_to_array($this->generatePaths(), true), |
||
66 | 'servers' => iterator_to_array($this->buildServers(), false), |
||
67 | 'components' => $this->buildComponents(), |
||
68 | ]); |
||
69 | } |
||
70 | |||
71 | /** |
||
72 | * @psalm-return Generator<int, Server> |
||
73 | */ |
||
74 | private function buildServers(): Generator |
||
75 | { |
||
76 | $isDev = YII_ENV_DEV; |
||
77 | |||
78 | foreach($this->hosts as $host) { |
||
79 | yield new Server([ |
||
80 | 'url' => "{protocol}://$host/", |
||
81 | 'variables' => [ |
||
82 | 'protocol' => new ServerVariable([ |
||
83 | 'enum' => ['http', 'https'], |
||
84 | 'default' => $isDev ? 'http' : 'https', |
||
85 | ]), |
||
86 | ], |
||
87 | ]); |
||
88 | } |
||
89 | } |
||
90 | |||
91 | /** |
||
92 | * @psalm-return Generator<int, PathItem> |
||
93 | */ |
||
94 | private function generatePaths(): Generator |
||
95 | { |
||
96 | foreach ($this->endpointRepository->getEndpointNames() as $name) { |
||
97 | try { |
||
98 | $endpoint = $this->endpointRepository->getByName($name); |
||
99 | $input = $endpoint->inputType; |
||
100 | |||
101 | yield "/$name" => new PathItem(array_filter([ |
||
102 | 'post' => new Operation([ |
||
103 | 'tags' => [], // TODO: parse endpoint name |
||
104 | 'summary' => $endpoint->description, |
||
105 | 'security' => iterator_to_array($this->generateSecuritySchemas($endpoint), true), |
||
106 | 'requestBody' => [ |
||
107 | 'required' => true, |
||
108 | 'content' => [ |
||
109 | 'application/x-www-form-urlencoded' => [ |
||
110 | 'schema' => $this->generateRequestSchema($endpoint), |
||
111 | ], |
||
112 | ], |
||
113 | ], |
||
114 | 'responses' => new Responses([ |
||
115 | 'default' => new Response([ |
||
116 | 'description' => '', |
||
117 | // TODO: response example |
||
118 | ]), |
||
119 | ]), |
||
120 | ]), |
||
121 | ])); |
||
122 | } catch (ConfigurationException $exception) { |
||
123 | } |
||
124 | } |
||
125 | } |
||
126 | |||
127 | private function buildComponents(): Components |
||
128 | { |
||
129 | return new Components([ |
||
130 | 'schemas' => $this->validationSchemas(), |
||
131 | 'securitySchemes' => $this->securitySchemes, |
||
132 | ]); |
||
133 | } |
||
134 | |||
135 | /** |
||
136 | * @return Schema[] |
||
137 | */ |
||
138 | private function validationSchemas(): array |
||
139 | { |
||
140 | $result = []; |
||
141 | foreach ($this->metValidators as $name => $className) { |
||
142 | $reflection = ValidatorReflection::fromClassname($className); |
||
143 | |||
144 | $result[$name] = new Schema(array_filter([ |
||
145 | 'pattern' => $reflection->getPattern(), |
||
146 | 'type' => 'string', |
||
147 | 'description' => $reflection->getSummary(), |
||
148 | 'example' => $reflection->getExample(), |
||
149 | ])); |
||
150 | } |
||
151 | |||
152 | return $result; |
||
153 | } |
||
154 | |||
155 | /** |
||
156 | * @param \hiapi\Core\Endpoint\Endpoint $endpoint |
||
157 | * @psalm-return Generator<int, SecurityScheme> |
||
158 | */ |
||
159 | private function generateSecuritySchemas(\hiapi\Core\Endpoint\Endpoint $endpoint): Generator |
||
160 | { |
||
161 | if (!empty($endpoint->permissions)) { |
||
162 | foreach ($this->securitySchemes as $name => $_) { |
||
163 | yield new SecurityRequirement([ |
||
164 | $name => $endpoint->permissions, |
||
165 | ]); |
||
166 | } |
||
167 | } |
||
168 | } |
||
169 | |||
170 | private function generateRequestSchema(\hiapi\Core\Endpoint\Endpoint $endpoint): Schema |
||
171 | { |
||
172 | $properties = $required = []; |
||
173 | |||
174 | $inputType = $endpoint->inputType; |
||
175 | if ($inputType instanceof Collection) { |
||
176 | // TODO: describe as a nested object |
||
177 | $inputType = $inputType->getEntriesClass(); |
||
178 | } |
||
179 | /** @psalm-var class-string<BaseCommand> $inputType */ |
||
180 | $reflection = BaseCommandReflection::fromClassname($inputType); |
||
181 | |||
182 | foreach ($reflection->getAttributes() as $attribute) { |
||
183 | $rules = $reflection->getAttributeValidationRules($attribute); |
||
184 | |||
185 | $schemas = []; |
||
186 | foreach ($rules as $key => $validator) { |
||
187 | if ($validator instanceof RequiredValidator) { |
||
188 | $required[] = $attribute; |
||
189 | } |
||
190 | if (($rule = $this->ruleNameByValidator($validator)) === null) { |
||
191 | continue; |
||
192 | } |
||
193 | |||
194 | $schemas[$key] = new Schema([ |
||
195 | '$ref' => '#/components/schemas/' . $rule, |
||
196 | ]); |
||
197 | } |
||
198 | if (count($schemas) === 0) { |
||
199 | continue; // Not validated attributes are not a part of a public API |
||
200 | } |
||
201 | |||
202 | if (count($schemas) === 1) { |
||
203 | $properties[$attribute] = reset($schemas); |
||
204 | } else { |
||
205 | $properties[$attribute] = new Schema(['allOf' => $schemas]); |
||
206 | } |
||
207 | } |
||
208 | |||
209 | return new Schema([ |
||
210 | 'properties' => $properties, |
||
211 | 'required' => array_unique($required), |
||
212 | ]); |
||
213 | } |
||
214 | |||
215 | /** @psalm-var array<string, class-string<Validator>> */ |
||
216 | private array $metValidators = []; |
||
217 | |||
218 | private function ruleNameByValidator(Validator $validator): ?string |
||
219 | { |
||
220 | $skippedValidators = [ |
||
221 | RequiredValidator::class => 'Is represented by OpenAPI required syntax', |
||
222 | EachValidator::class => 'Is represented by OpenAPI array syntax', |
||
223 | InlineValidator::class => 'Could not be inspected and should not be used', |
||
224 | SafeValidator::class => 'Means nothing in OpenAPI', |
||
225 | ]; |
||
226 | if (isset($skippedValidators[get_class($validator)])) { |
||
227 | return null; |
||
228 | } |
||
229 | |||
230 | $name = (new ReflectionObject($validator))->getShortName(); |
||
231 | if (!isset($this->metValidators[$name])) { |
||
232 | $this->metValidators[$name] = get_class($validator); |
||
233 | } |
||
234 | |||
235 | return $name; |
||
236 | } |
||
237 | } |
||
238 |