| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  | <?php | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | namespace BookStack\Api; | 
					
						
							| 
									
										
										
										
											2020-01-13 00:25:14 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | use BookStack\Http\Controllers\Api\ApiController; | 
					
						
							| 
									
										
										
										
											2022-02-04 00:52:28 +08:00
										 |  |  | use Exception; | 
					
						
							| 
									
										
										
										
											2020-11-22 01:03:24 +08:00
										 |  |  | use Illuminate\Contracts\Container\BindingResolutionException; | 
					
						
							| 
									
										
										
										
											2020-01-13 00:25:14 +08:00
										 |  |  | use Illuminate\Support\Collection; | 
					
						
							| 
									
										
										
										
											2020-11-22 01:03:24 +08:00
										 |  |  | use Illuminate\Support\Facades\Cache; | 
					
						
							| 
									
										
										
										
											2020-01-13 00:25:14 +08:00
										 |  |  | use Illuminate\Support\Facades\Route; | 
					
						
							| 
									
										
										
										
											2020-04-10 23:05:17 +08:00
										 |  |  | use Illuminate\Support\Str; | 
					
						
							| 
									
										
										
										
											2022-02-04 00:52:28 +08:00
										 |  |  | use Illuminate\Validation\Rules\Password; | 
					
						
							| 
									
										
										
										
											2020-01-13 00:25:14 +08:00
										 |  |  | use ReflectionClass; | 
					
						
							|  |  |  | use ReflectionException; | 
					
						
							|  |  |  | use ReflectionMethod; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class ApiDocsGenerator | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     protected $reflectionClasses = []; | 
					
						
							|  |  |  |     protected $controllerClasses = []; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-22 01:03:24 +08:00
										 |  |  |     /** | 
					
						
							|  |  |  |      * Load the docs form the cache if existing | 
					
						
							|  |  |  |      * otherwise generate and store in the cache. | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     public static function generateConsideringCache(): Collection | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $appVersion = trim(file_get_contents(base_path('version'))); | 
					
						
							|  |  |  |         $cacheKey = 'api-docs::' . $appVersion; | 
					
						
							|  |  |  |         if (Cache::has($cacheKey) && config('app.env') === 'production') { | 
					
						
							|  |  |  |             $docs = Cache::get($cacheKey); | 
					
						
							|  |  |  |         } else { | 
					
						
							| 
									
										
										
										
											2021-11-06 00:27:59 +08:00
										 |  |  |             $docs = (new ApiDocsGenerator())->generate(); | 
					
						
							| 
									
										
										
										
											2020-11-22 01:03:24 +08:00
										 |  |  |             Cache::put($cacheKey, $docs, 60 * 24); | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-22 01:03:24 +08:00
										 |  |  |         return $docs; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-01-13 00:25:14 +08:00
										 |  |  |     /** | 
					
						
							|  |  |  |      * Generate API documentation. | 
					
						
							|  |  |  |      */ | 
					
						
							| 
									
										
										
										
											2020-11-22 01:03:24 +08:00
										 |  |  |     protected function generate(): Collection | 
					
						
							| 
									
										
										
										
											2020-01-13 00:25:14 +08:00
										 |  |  |     { | 
					
						
							|  |  |  |         $apiRoutes = $this->getFlatApiRoutes(); | 
					
						
							|  |  |  |         $apiRoutes = $this->loadDetailsFromControllers($apiRoutes); | 
					
						
							|  |  |  |         $apiRoutes = $this->loadDetailsFromFiles($apiRoutes); | 
					
						
							|  |  |  |         $apiRoutes = $apiRoutes->groupBy('base_model'); | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-01-13 00:25:14 +08:00
										 |  |  |         return $apiRoutes; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * Load any API details stored in static files. | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     protected function loadDetailsFromFiles(Collection $routes): Collection | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         return $routes->map(function (array $route) { | 
					
						
							| 
									
										
										
										
											2020-01-18 17:48:30 +08:00
										 |  |  |             $exampleTypes = ['request', 'response']; | 
					
						
							| 
									
										
										
										
											2021-11-15 00:28:01 +08:00
										 |  |  |             $fileTypes = ['json', 'http']; | 
					
						
							| 
									
										
										
										
											2020-01-18 17:48:30 +08:00
										 |  |  |             foreach ($exampleTypes as $exampleType) { | 
					
						
							| 
									
										
										
										
											2021-11-15 00:28:01 +08:00
										 |  |  |                 foreach ($fileTypes as $fileType) { | 
					
						
							|  |  |  |                     $exampleFile = base_path("dev/api/{$exampleType}s/{$route['name']}." . $fileType); | 
					
						
							|  |  |  |                     if (file_exists($exampleFile)) { | 
					
						
							|  |  |  |                         $route["example_{$exampleType}"] = file_get_contents($exampleFile); | 
					
						
							|  |  |  |                         continue 2; | 
					
						
							|  |  |  |                     } | 
					
						
							|  |  |  |                 } | 
					
						
							|  |  |  |                 $route["example_{$exampleType}"] = null; | 
					
						
							| 
									
										
										
										
											2020-01-18 17:48:30 +08:00
										 |  |  |             } | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-01-13 00:25:14 +08:00
										 |  |  |             return $route; | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * Load any details we can fetch from the controller and its methods. | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     protected function loadDetailsFromControllers(Collection $routes): Collection | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         return $routes->map(function (array $route) { | 
					
						
							|  |  |  |             $method = $this->getReflectionMethod($route['controller'], $route['controller_method']); | 
					
						
							|  |  |  |             $comment = $method->getDocComment(); | 
					
						
							|  |  |  |             $route['description'] = $comment ? $this->parseDescriptionFromMethodComment($comment) : null; | 
					
						
							|  |  |  |             $route['body_params'] = $this->getBodyParamsFromClass($route['controller'], $route['controller_method']); | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-01-13 00:25:14 +08:00
										 |  |  |             return $route; | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * Load body params and their rules by inspecting the given class and method name. | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  |      * | 
					
						
							| 
									
										
										
										
											2020-11-22 01:03:24 +08:00
										 |  |  |      * @throws BindingResolutionException | 
					
						
							| 
									
										
										
										
											2020-01-13 00:25:14 +08:00
										 |  |  |      */ | 
					
						
							|  |  |  |     protected function getBodyParamsFromClass(string $className, string $methodName): ?array | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         /** @var ApiController $class */ | 
					
						
							|  |  |  |         $class = $this->controllerClasses[$className] ?? null; | 
					
						
							|  |  |  |         if ($class === null) { | 
					
						
							|  |  |  |             $class = app()->make($className); | 
					
						
							|  |  |  |             $this->controllerClasses[$className] = $class; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-02-04 00:52:28 +08:00
										 |  |  |         $rules = collect($class->getValidationRules()[$methodName] ?? [])->map(function($validations) { | 
					
						
							|  |  |  |             return array_map(function($validation) { | 
					
						
							|  |  |  |                 return $this->getValidationAsString($validation); | 
					
						
							|  |  |  |             }, $validations); | 
					
						
							|  |  |  |         })->toArray(); | 
					
						
							| 
									
										
										
										
											2021-11-05 08:28:41 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-05 08:26:55 +08:00
										 |  |  |         return empty($rules) ? null : $rules; | 
					
						
							| 
									
										
										
										
											2020-01-13 00:25:14 +08:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-02-04 00:52:28 +08:00
										 |  |  |     /** | 
					
						
							|  |  |  |      * Convert the given validation message to a readable string. | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     protected function getValidationAsString($validation): string | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         if (is_string($validation)) { | 
					
						
							|  |  |  |             return $validation; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (is_object($validation) && method_exists($validation, '__toString')) { | 
					
						
							|  |  |  |             return strval($validation); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if ($validation instanceof Password) { | 
					
						
							|  |  |  |             return 'min:8'; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         $class = get_class($validation); | 
					
						
							|  |  |  |         throw new Exception("Cannot provide string representation of rule for class: {$class}"); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-01-13 00:25:14 +08:00
										 |  |  |     /** | 
					
						
							|  |  |  |      * Parse out the description text from a class method comment. | 
					
						
							|  |  |  |      */ | 
					
						
							| 
									
										
										
										
											2021-11-05 08:26:55 +08:00
										 |  |  |     protected function parseDescriptionFromMethodComment(string $comment): string | 
					
						
							| 
									
										
										
										
											2020-01-13 00:25:14 +08:00
										 |  |  |     { | 
					
						
							|  |  |  |         $matches = []; | 
					
						
							|  |  |  |         preg_match_all('/^\s*?\*\s((?![@\s]).*?)$/m', $comment, $matches); | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-01-13 00:25:14 +08:00
										 |  |  |         return implode(' ', $matches[1] ?? []); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * Get a reflection method from the given class name and method name. | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  |      * | 
					
						
							| 
									
										
										
										
											2020-01-13 00:25:14 +08:00
										 |  |  |      * @throws ReflectionException | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     protected function getReflectionMethod(string $className, string $methodName): ReflectionMethod | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $class = $this->reflectionClasses[$className] ?? null; | 
					
						
							|  |  |  |         if ($class === null) { | 
					
						
							|  |  |  |             $class = new ReflectionClass($className); | 
					
						
							|  |  |  |             $this->reflectionClasses[$className] = $class; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return $class->getMethod($methodName); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * Get the system API routes, formatted into a flat collection. | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     protected function getFlatApiRoutes(): Collection | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         return collect(Route::getRoutes()->getRoutes())->filter(function ($route) { | 
					
						
							|  |  |  |             return strpos($route->uri, 'api/') === 0; | 
					
						
							|  |  |  |         })->map(function ($route) { | 
					
						
							|  |  |  |             [$controller, $controllerMethod] = explode('@', $route->action['uses']); | 
					
						
							| 
									
										
										
										
											2020-01-16 04:18:02 +08:00
										 |  |  |             $baseModelName = explode('.', explode('/', $route->uri)[1])[0]; | 
					
						
							| 
									
										
										
										
											2020-01-13 00:25:14 +08:00
										 |  |  |             $shortName = $baseModelName . '-' . $controllerMethod; | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-01-13 00:25:14 +08:00
										 |  |  |             return [ | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  |                 'name'                    => $shortName, | 
					
						
							|  |  |  |                 'uri'                     => $route->uri, | 
					
						
							|  |  |  |                 'method'                  => $route->methods[0], | 
					
						
							|  |  |  |                 'controller'              => $controller, | 
					
						
							|  |  |  |                 'controller_method'       => $controllerMethod, | 
					
						
							| 
									
										
										
										
											2020-04-10 23:05:17 +08:00
										 |  |  |                 'controller_method_kebab' => Str::kebab($controllerMethod), | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  |                 'base_model'              => $baseModelName, | 
					
						
							| 
									
										
										
										
											2020-01-13 00:25:14 +08:00
										 |  |  |             ]; | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2021-03-08 06:24:05 +08:00
										 |  |  | } |