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 | |||
3 | namespace Yaro\ApiDocs; |
||
4 | |||
5 | use ReflectionClass; |
||
6 | use Illuminate\Routing\Router; |
||
7 | use Illuminate\Contracts\Config\Repository as Config; |
||
8 | use Illuminate\Http\Request; |
||
9 | use Yaro\ApiDocs\Blueprint; |
||
10 | |||
11 | class ApiDocs |
||
12 | { |
||
13 | |||
14 | private $router; |
||
15 | private $config; |
||
16 | private $request; |
||
17 | |||
18 | public function __construct(Router $router, Config $config, Request $request) |
||
19 | { |
||
20 | $this->router = $router; |
||
21 | $this->config = $config; |
||
22 | $this->request = $request; |
||
23 | } // end __construct |
||
24 | |||
25 | public function show($routePrefix = null) |
||
26 | { |
||
27 | $currentPrefix = $this->request->get('prefix', $this->getRoutePrefix($routePrefix)); |
||
28 | $endpoints = $this->getEndpoints($currentPrefix); |
||
29 | $endpoints = $this->getSortedEndpoints($endpoints); |
||
30 | $prefixes = $this->getRoutePrefixes(); |
||
31 | |||
32 | return view('apidocs::docs', compact('endpoints', 'prefixes', 'currentPrefix')); |
||
33 | } // end show |
||
34 | |||
35 | public function blueprint($routePrefix = null) |
||
36 | { |
||
37 | $routePrefix = $this->request->get('prefix', $this->getRoutePrefix($routePrefix)); |
||
38 | |||
39 | $blueprint = app()->make(Blueprint::class); |
||
40 | $blueprint->setRoutePrefix($routePrefix); |
||
41 | $blueprint->setEndpoints($this->getEndpoints($routePrefix)); |
||
42 | |||
43 | return $blueprint; |
||
44 | } // end blueprint |
||
45 | |||
46 | private function getEndpoints($routePrefix) |
||
47 | { |
||
48 | $endpoints = []; |
||
49 | |||
50 | foreach ($this->router->getRoutes() as $route) { |
||
51 | if (!$this->isPrefixedRoute($route, $routePrefix) || $this->isClosureRoute($route) || $this->isExcluded($route)) { |
||
52 | continue; |
||
53 | } |
||
54 | |||
55 | $actionController = explode("@", $this->getRouteParam($route, 'action.controller')); |
||
56 | $class = $actionController[0]; |
||
57 | $method = $actionController[1]; |
||
58 | |||
59 | if (!class_exists($class) || !method_exists($class, $method)) { |
||
60 | continue; |
||
61 | } |
||
62 | |||
63 | |||
64 | list($title, $description, $params) = $this->getRouteDocBlock($class, $method); |
||
65 | $key = $this->generateEndpointKey($class); |
||
66 | |||
67 | $endpoints[$key][] = [ |
||
68 | 'hash' => $this->generateHashForUrl($key, $route, $method), |
||
69 | 'uri' => $this->getRouteParam($route, 'uri'), |
||
70 | 'name' => $method, |
||
71 | 'methods' => $this->getRouteParam($route, 'methods'), |
||
72 | 'docs' => [ |
||
73 | 'title' => $title, |
||
74 | 'description' => trim($description), |
||
75 | 'params' => $params, |
||
76 | 'uri_params' => $this->getUriParams($route), |
||
77 | ], |
||
78 | ]; |
||
79 | } |
||
80 | |||
81 | return $endpoints; |
||
82 | } // end getEndpoints |
||
83 | |||
84 | private function isExcluded($route) |
||
85 | { |
||
86 | $uri = $this->getRouteParam($route, 'uri'); |
||
87 | $actionController = $this->getRouteParam($route, 'action.controller'); |
||
88 | |||
89 | return $this->isExcludedClass($actionController) || $this->isExcludedRoute($uri); |
||
90 | } // end isExcluded |
||
91 | |||
92 | private function isExcludedRoute($uri) |
||
93 | { |
||
94 | foreach ($this->config->get('yaro.apidocs.exclude.routes', []) as $pattern) { |
||
95 | if (str_is($pattern, $uri)) { |
||
96 | return true; |
||
97 | } |
||
98 | } |
||
99 | |||
100 | return false; |
||
101 | } // end isExcludedRoute |
||
102 | |||
103 | private function isExcludedClass($actionController) |
||
104 | { |
||
105 | foreach ($this->config->get('yaro.apidocs.exclude.classes', []) as $pattern) { |
||
106 | if (str_is($pattern, $actionController)) { |
||
107 | return true; |
||
108 | } |
||
109 | } |
||
110 | |||
111 | return false; |
||
112 | } // end isExcludedClass |
||
113 | |||
114 | private function isPrefixedRoute($route, $routePrefix) |
||
115 | { |
||
116 | $regexp = '~^'. preg_quote($routePrefix) .'~'; |
||
117 | |||
118 | return preg_match($regexp, $this->getRouteParam($route, 'uri')); |
||
119 | } // end isPrefixedRoute |
||
120 | |||
121 | private function getRoutePrefix($routePrefix) |
||
122 | { |
||
123 | $prefixes = $this->getRoutePrefixes(); |
||
124 | |||
125 | return in_array($routePrefix, $prefixes) ? $routePrefix : array_shift($prefixes); |
||
126 | } |
||
127 | |||
128 | private function getRoutePrefixes() |
||
129 | { |
||
130 | $prefixes = $this->config->get('yaro.apidocs.prefix', 'api'); |
||
131 | if (!is_array($prefixes)) { |
||
132 | $prefixes = [$prefixes]; |
||
133 | } |
||
134 | |||
135 | return $prefixes; |
||
136 | } |
||
137 | |||
138 | private function isClosureRoute($route) |
||
139 | { |
||
140 | $action = $this->getRouteParam($route, 'action.uses'); |
||
141 | |||
142 | return is_object($action); |
||
143 | } // end isClosureRoute |
||
144 | |||
145 | private function getRouteDocBlock($class, $method) |
||
146 | { |
||
147 | $reflector = new ReflectionClass($class); |
||
148 | |||
149 | $title = implode(' ', $this->splitCamelCaseToWords($method)); |
||
150 | $title = ucfirst(strtolower($title)); |
||
151 | $description = ''; |
||
152 | $params = []; |
||
153 | |||
154 | $reflectorMethod = $reflector->getMethod($method); |
||
155 | $docs = explode("\n", $reflectorMethod->getDocComment()); |
||
156 | $docs = array_filter($docs); |
||
157 | if (!$docs) { |
||
0 ignored issues
–
show
|
|||
158 | return [$title, $description, $params]; |
||
159 | } |
||
160 | |||
161 | $docs = $this->filterDocBlock($docs); |
||
162 | |||
163 | $title = array_shift($docs); |
||
164 | |||
165 | $checkForLongDescription = true; |
||
166 | foreach ($docs as $line) { |
||
167 | if ($checkForLongDescription && !preg_match('~^@\w+~', $line)) { |
||
168 | $description .= trim($line) .' '; |
||
169 | } elseif (preg_match('~^@\w+~', $line)) { |
||
170 | $checkForLongDescription = false; |
||
171 | if (preg_match('~^@param~', $line)) { |
||
172 | $paramChunks = $this->getParamChunksFromLine($line); |
||
173 | |||
174 | $paramType = array_shift($paramChunks); |
||
175 | $paramName = substr(array_shift($paramChunks), 1); |
||
176 | $params[$paramName] = [ |
||
177 | 'type' => $paramType, |
||
178 | 'name' => $paramName, |
||
179 | 'description' => implode(' ', $paramChunks), |
||
180 | 'template' => $this->getParamTemplateByType($paramType), |
||
181 | ]; |
||
182 | } |
||
183 | } |
||
184 | } |
||
185 | |||
186 | // TODO: |
||
187 | $rules = []; |
||
188 | foreach($reflectorMethod->getParameters() as $reflectorParam) { |
||
189 | $paramClass = $reflectorParam->getClass(); |
||
190 | if ($paramClass instanceof ReflectionClass) { |
||
191 | $name = $paramClass->getName(); |
||
0 ignored issues
–
show
![]() |
|||
192 | $paramClassInstance = new $name; |
||
193 | if (is_a($paramClassInstance, Request::class) && method_exists($paramClassInstance, 'rules')) { |
||
194 | $paramClassInstance->__apidocs = true; |
||
195 | $rules = $paramClassInstance->rules(); |
||
196 | } |
||
197 | } |
||
198 | } |
||
199 | |||
200 | $params = $this->fillParamsWithRequestRules($params, $rules); |
||
201 | |||
202 | foreach ($params as $name => $param) { |
||
203 | $params[$name]['rules'] = $rules[$name] ?? []; |
||
204 | if (is_string($params[$name]['rules'])) { |
||
205 | $params[$name]['rules'] = explode('|', $params[$name]['rules']); |
||
206 | } elseif (is_array($params[$name]['rules'])) { |
||
207 | $params[$name]['rules'] = $params[$name]['rules']; |
||
208 | } |
||
209 | } |
||
210 | |||
211 | return [$title, $description, $params]; |
||
212 | } // end getRouteDocBlock |
||
213 | |||
214 | private function fillParamsWithRequestRules($params, $rules) |
||
215 | { |
||
216 | foreach ($rules as $paramName => $rule) { |
||
217 | if (isset($params[$paramName])) { |
||
218 | continue; |
||
219 | } |
||
220 | |||
221 | $params[$paramName] = [ |
||
222 | 'type' => 'string', |
||
223 | 'name' => $paramName, |
||
224 | 'description' => '', |
||
225 | 'template' => $this->getParamTemplateByType('string'), |
||
226 | ]; |
||
227 | } |
||
228 | |||
229 | return $params; |
||
230 | } |
||
231 | |||
232 | private function getParamTemplateByType($paramType) |
||
233 | { |
||
234 | switch ($paramType) { |
||
235 | case 'file': |
||
236 | return 'file'; |
||
237 | |||
238 | case 'bool': |
||
239 | case 'boolean': |
||
240 | return 'boolean'; |
||
241 | |||
242 | case 'int': |
||
243 | return 'integer'; |
||
244 | |||
245 | case 'text': |
||
246 | case 'string': |
||
247 | default: |
||
248 | return 'string'; |
||
249 | } |
||
250 | } // end getParamTemplateByType |
||
251 | |||
252 | private function getParamChunksFromLine($line) |
||
253 | { |
||
254 | $paramChunks = explode(' ', $line); |
||
255 | $paramChunks = array_filter($paramChunks, function($val) { |
||
256 | return $val !== ''; |
||
257 | }); |
||
258 | unset($paramChunks[0]); |
||
259 | |||
260 | return $paramChunks; |
||
261 | } // end getParamChunksFromLine |
||
262 | |||
263 | private function filterDocBlock($docs) |
||
264 | { |
||
265 | foreach ($docs as &$line) { |
||
266 | $line = preg_replace('~\s*\*\s*~', '', $line); |
||
267 | $line = preg_replace('~^/$~', '', $line); |
||
268 | } |
||
269 | $docs = array_values(array_filter($docs)); |
||
270 | |||
271 | return $docs; |
||
272 | } // end filterDocBlock |
||
273 | |||
274 | private function generateEndpointKey($class) |
||
275 | { |
||
276 | $disabledNamespaces = $this->config->get('yaro.apidocs.disabled_namespaces', []); |
||
277 | |||
278 | $chunks = explode('\\', $class); |
||
279 | foreach ($chunks as $index => $chunk) { |
||
280 | if (in_array($chunk, $disabledNamespaces)) { |
||
281 | unset($chunks[$index]); |
||
282 | continue; |
||
283 | } |
||
284 | |||
285 | $chunk = preg_replace('~Controller$~', '', $chunk); |
||
286 | if ($chunk) { |
||
287 | $chunk = $this->splitCamelCaseToWords($chunk); |
||
288 | $chunks[$index] = implode(' ', $chunk); |
||
289 | } |
||
290 | } |
||
291 | |||
292 | return implode('.', $chunks); |
||
293 | } // end generateEndpointKey |
||
294 | |||
295 | private function getSortedEndpoints($endpoints) |
||
296 | { |
||
297 | ksort($endpoints); |
||
298 | |||
299 | $sorted = array(); |
||
300 | foreach ($endpoints as $key => $val) { |
||
301 | $this->ins($sorted, explode('.', $key), $val); |
||
302 | } |
||
303 | |||
304 | return $sorted; |
||
305 | } // end getSortedEndpoints |
||
306 | |||
307 | private function getUriParams($route) |
||
308 | { |
||
309 | preg_match_all('~{(\w+)}~', $this->getRouteParam($route, 'uri'), $matches); |
||
310 | |||
311 | return isset($matches[1]) ? $matches[1] : []; |
||
312 | } // end getUriParams |
||
313 | |||
314 | private function generateHashForUrl($key, $route, $method) |
||
315 | { |
||
316 | $path = preg_replace('~\s+~', '-', $key); |
||
317 | $httpMethod = $this->getRouteParam($route, 'methods.0'); |
||
318 | $classMethod = implode('-', $this->splitCamelCaseToWords($method)); |
||
319 | |||
320 | $hash = $path .'::'. $httpMethod .'::'. $classMethod; |
||
321 | |||
322 | return strtolower($hash); |
||
323 | } // end generateHashForUrl |
||
324 | |||
325 | private function splitCamelCaseToWords($chunk) |
||
326 | { |
||
327 | $splitCamelCaseRegexp = '/(?#! splitCamelCase Rev:20140412) |
||
328 | # Split camelCase "words". Two global alternatives. Either g1of2: |
||
329 | (?<=[a-z]) # Position is after a lowercase, |
||
330 | (?=[A-Z]) # and before an uppercase letter. |
||
331 | | (?<=[A-Z]) # Or g2of2; Position is after uppercase, |
||
332 | (?=[A-Z][a-z]) # and before upper-then-lower case. |
||
333 | /x'; |
||
334 | |||
335 | return preg_split($splitCamelCaseRegexp, $chunk); |
||
336 | } // end splitCamelCaseToWords |
||
337 | |||
338 | private function getRouteParam($route, $param) |
||
339 | { |
||
340 | $route = (array) $route; |
||
341 | $prefix = chr(0).'*'.chr(0); |
||
342 | |||
343 | return $this->arrayGet( |
||
344 | $route, |
||
345 | $prefix.$param, |
||
346 | $this->arrayGet($route, $param) |
||
347 | ); |
||
348 | } // end getRouteParam |
||
349 | |||
350 | private function ins(&$ary, $keys, $val) |
||
351 | { |
||
352 | $keys ? |
||
353 | $this->ins($ary[array_shift($keys)], $keys, $val) : |
||
354 | $ary = $val; |
||
355 | } // end ins |
||
356 | |||
357 | private function arrayGet($array, $key, $default = null) |
||
358 | { |
||
359 | if (is_null($key)) { |
||
360 | return $array; |
||
361 | } |
||
362 | |||
363 | if (isset($array[$key])) { |
||
364 | return $array[$key]; |
||
365 | } |
||
366 | |||
367 | foreach (explode('.', $key) as $segment) { |
||
368 | if (!is_array($array) || ! array_key_exists($segment, $array)) { |
||
369 | return $default; |
||
370 | } |
||
371 | $array = $array[$segment]; |
||
372 | } |
||
373 | |||
374 | return $array; |
||
375 | } |
||
376 | |||
377 | } |
||
378 |
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.