Issues (2)

Security Analysis    no request data  

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/ApiDocs.php (2 issues)

Upgrade to new PHP Analysis Engine

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
Bug Best Practice introduced by
The expression $docs of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
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
Consider using $paramClass->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
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