![]() Server : Apache/2.4.62 (Unix) OpenSSL/1.0.2k-fips System : Linux server1.richineiserver.com 3.10.0-1160.119.1.el7.x86_64 #1 SMP Tue Jun 4 14:43:51 UTC 2024 x86_64 User : fdshops ( 1003) PHP Version : 8.2.21 Disable Function : NONE Directory : /home/fdshops/public_html/vendor/knuckleswtf/scribe/src/Extracting/ |
<?php namespace Knuckles\Scribe\Extracting; use Knuckles\Camel\Extraction\ExtractedEndpointData; use Knuckles\Camel\Extraction\Metadata; use Knuckles\Camel\Extraction\Parameter; use Faker\Factory; use Illuminate\Http\Testing\File; use Illuminate\Http\UploadedFile; use Illuminate\Routing\Route; use Illuminate\Support\Arr; use Illuminate\Support\Str; use Knuckles\Camel\Extraction\ResponseCollection; use Knuckles\Camel\Extraction\ResponseField; use Knuckles\Camel\Output\OutputEndpointData; use Knuckles\Scribe\Tools\DocumentationConfig; use Knuckles\Scribe\Tools\RoutePatternMatcher; class Extractor { private DocumentationConfig $config; use ParamHelpers; private static ?Route $routeBeingProcessed = null; public function __construct(DocumentationConfig $config = null) { // If no config is injected, pull from global $this->config = $config ?: new DocumentationConfig(config('scribe')); } /** * External interface that allows users to know what route is currently being processed */ public static function getRouteBeingProcessed(): ?Route { return self::$routeBeingProcessed; } /** * @param Route $route * @param array $routeRules Rules to apply when generating documentation for this route * * @return ExtractedEndpointData * */ public function processRoute(Route $route, array $routeRules = []): ExtractedEndpointData { self::$routeBeingProcessed = $route; $endpointData = ExtractedEndpointData::fromRoute($route); $inheritedDocsOverrides = []; if ($endpointData->controller->hasMethod('inheritedDocsOverrides')) { $inheritedDocsOverrides = call_user_func([$endpointData->controller->getName(), 'inheritedDocsOverrides']); $inheritedDocsOverrides = $inheritedDocsOverrides[$endpointData->method->getName()] ?? []; } $this->fetchMetadata($endpointData, $routeRules); $this->mergeInheritedMethodsData('metadata', $endpointData, $inheritedDocsOverrides); $this->fetchUrlParameters($endpointData, $routeRules); $this->mergeInheritedMethodsData('urlParameters', $endpointData, $inheritedDocsOverrides); $endpointData->cleanUrlParameters = self::cleanParams($endpointData->urlParameters); $this->addAuthField($endpointData); $this->fetchQueryParameters($endpointData, $routeRules); $this->mergeInheritedMethodsData('queryParameters', $endpointData, $inheritedDocsOverrides); $endpointData->cleanQueryParameters = self::cleanParams($endpointData->queryParameters); $this->fetchRequestHeaders($endpointData, $routeRules); $this->mergeInheritedMethodsData('headers', $endpointData, $inheritedDocsOverrides); $this->fetchBodyParameters($endpointData, $routeRules); $endpointData->cleanBodyParameters = self::cleanParams($endpointData->bodyParameters); $this->mergeInheritedMethodsData('bodyParameters', $endpointData, $inheritedDocsOverrides); if (count($endpointData->cleanBodyParameters) && !isset($endpointData->headers['Content-Type'])) { // Set content type if the user forgot to set it $endpointData->headers['Content-Type'] = 'application/json'; } // We need to do all this so response calls can work correctly, // even though they're only needed for output // Note that this [$files, $regularParameters] = OutputEndpointData::splitIntoFileAndRegularParameters($endpointData->cleanBodyParameters); if (count($files)) { $endpointData->headers['Content-Type'] = 'multipart/form-data'; } $endpointData->fileParameters = $files; $endpointData->cleanBodyParameters = $regularParameters; $this->fetchResponses($endpointData, $routeRules); $this->mergeInheritedMethodsData('responses', $endpointData, $inheritedDocsOverrides); $this->fetchResponseFields($endpointData, $routeRules); $this->mergeInheritedMethodsData('responseFields', $endpointData, $inheritedDocsOverrides); self::$routeBeingProcessed = null; return $endpointData; } protected function fetchMetadata(ExtractedEndpointData $endpointData, array $rulesToApply): void { $endpointData->metadata = new Metadata([ 'groupName' => $this->config->get('groups.default', ''), "authenticated" => $this->config->get("auth.default", false), ]); $this->iterateThroughStrategies('metadata', $endpointData, $rulesToApply, function ($results) use ($endpointData) { foreach ($results as $key => $item) { $hadPreviousValue = !is_null($endpointData->metadata->$key); $noNewValueSet = is_null($item) || $item === ""; if ($hadPreviousValue && $noNewValueSet) { continue; } $endpointData->metadata->$key = $item; } }); } protected function fetchUrlParameters(ExtractedEndpointData $endpointData, array $rulesToApply): void { $this->iterateThroughStrategies('urlParameters', $endpointData, $rulesToApply, function ($results) use ($endpointData) { foreach ($results as $key => $item) { if (empty($item['name'])) { $item['name'] = $key; } $endpointData->urlParameters[$key] = Parameter::create($item, $endpointData->urlParameters[$key] ?? []); } }); } protected function fetchQueryParameters(ExtractedEndpointData $endpointData, array $rulesToApply): void { $this->iterateThroughStrategies('queryParameters', $endpointData, $rulesToApply, function ($results) use ($endpointData) { foreach ($results as $key => $item) { if (empty($item['name'])) { $item['name'] = $key; } $endpointData->queryParameters[$key] = Parameter::create($item, $endpointData->queryParameters[$key] ?? []); } }); } protected function fetchBodyParameters(ExtractedEndpointData $endpointData, array $rulesToApply): void { $this->iterateThroughStrategies('bodyParameters', $endpointData, $rulesToApply, function ($results) use ($endpointData) { foreach ($results as $key => $item) { if (empty($item['name'])) { $item['name'] = $key; } $endpointData->bodyParameters[$key] = Parameter::create($item, $endpointData->bodyParameters[$key] ?? []); } }); } protected function fetchResponses(ExtractedEndpointData $endpointData, array $rulesToApply): void { $this->iterateThroughStrategies('responses', $endpointData, $rulesToApply, function ($results) use ($endpointData) { // Responses from different strategies are all added, not overwritten $endpointData->responses->concat($results); }); // Ensure 200 responses come first $endpointData->responses = new ResponseCollection($endpointData->responses->sortBy('status')->values()); } protected function fetchResponseFields(ExtractedEndpointData $endpointData, array $rulesToApply): void { $this->iterateThroughStrategies('responseFields', $endpointData, $rulesToApply, function ($results) use ($endpointData) { foreach ($results as $key => $item) { $endpointData->responseFields[$key] = Parameter::create($item, $endpointData->responseFields[$key] ?? []); } }); } protected function fetchRequestHeaders(ExtractedEndpointData $endpointData, array $rulesToApply): void { $this->iterateThroughStrategies('headers', $endpointData, $rulesToApply, function ($results) use ($endpointData) { foreach ($results as $key => $item) { if ($item) { $endpointData->headers[$key] = $item; } } }); } /** * Iterate through all defined strategies for this stage. * A strategy may return an array of attributes * to be added to that stage data, or it may modify the stage data directly. * * @param string $stage * @param ExtractedEndpointData $endpointData * @param array $rulesToApply * @param callable $handler Function to run after each strategy returns its results (an array). * */ protected function iterateThroughStrategies( string $stage, ExtractedEndpointData $endpointData, array $rulesToApply, callable $handler ): void { $strategies = $this->config->get("strategies.$stage", []); $overrides = []; foreach ($strategies as $strategyClassOrTuple) { if (is_array($strategyClassOrTuple)) { [$strategyClass, $settings] = $strategyClassOrTuple; if ($strategyClass == 'override') { $overrides = $settings; continue; } $settings = self::transformOldRouteRulesIntoNewSettings($stage, $rulesToApply, $strategyClass, $settings); $routesToSkip = $settings["except"] ?? []; $routesToInclude = $settings["only"] ?? []; if (!empty($routesToSkip)) { if (RoutePatternMatcher::matches($endpointData->route, $routesToSkip)) { continue; } } elseif (!empty($routesToInclude)) { if (!RoutePatternMatcher::matches($endpointData->route, $routesToInclude)) { continue; } } } else { $strategyClass = $strategyClassOrTuple; $settings = $rulesToApply; } $strategy = new $strategyClass($this->config); $results = $strategy($endpointData, $settings); if (is_array($results)) { $handler($results); } } if (!empty($overrides)) { $handler($overrides); } } /** * This method prepares and simplifies request parameters for use in example requests and response calls. * It takes in an array with rich details about a parameter eg * ['age' => new Parameter([ * 'description' => 'The age', * 'example' => 12, * 'required' => false, * ])] * And transforms them into key-example pairs : ['age' => 12] * It also filters out parameters which have null values and have 'required' as false. * It converts all file params that have string examples to actual files (instances of UploadedFile). * * @param array<string,Parameter> $parameters * * @return array */ public static function cleanParams(array $parameters): array { $cleanParameters = []; /** * @var string $paramName * @var Parameter $details */ foreach ($parameters as $paramName => $details) { // Remove params which have no intentional examples and are optional. if (!$details->exampleWasSpecified) { if (is_null($details->example) && $details->required === false) { continue; } } if ($details->type === 'file') { if (is_string($details->example)) { $details->example = self::convertStringValueToUploadedFileInstance($details->example); } else if (is_null($details->example)) { $details->example = (new self)->generateDummyValue($details->type); } } if (Str::startsWith($paramName, '[].')) { // Entire body is an array if (empty($parameters["[]"])) { // Make sure there's a parent $cleanParameters["[]"] = [[], []]; $parameters["[]"] = new Parameter([ "name" => "[]", "type" => "object[]", "description" => "", "required" => true, "example" => [$paramName => $details->example], ]); } } if (Str::contains($paramName, '.')) { // Object field (or array of objects) self::setObject($cleanParameters, $paramName, $details->example, $parameters, $details->required); } else { $cleanParameters[$paramName] = $details->example; } } // Finally, if the body is an array, flatten it. if (isset($cleanParameters['[]'])) { $cleanParameters = $cleanParameters['[]']; } return $cleanParameters; } public static function setObject(array &$results, string $path, $value, array $source, bool $isRequired) { $parts = explode('.', $path); array_pop($parts); // Get rid of the field name $baseName = join('.', $parts); // For array fields, the type should be indicated in the source object by now; // eg test.items[] would actually be described as name: test.items, type: object[] // So we get rid of that ending [] // For other fields (eg test.items[].name), it remains as-is $baseNameInOriginalParams = $baseName; while (Str::endsWith($baseNameInOriginalParams, '[]')) { $baseNameInOriginalParams = substr($baseNameInOriginalParams, 0, -2); } // When the body is an array, param names will be "[].paramname", // so $baseNameInOriginalParams here will be empty if (Str::startsWith($path, '[].')) { $baseNameInOriginalParams = '[]'; } if (Arr::has($source, $baseNameInOriginalParams)) { /** @var Parameter $parentData */ $parentData = Arr::get($source, $baseNameInOriginalParams); // Path we use for data_set $dotPath = str_replace('[]', '.0', $path); // Don't overwrite parent if there's already data there if ($parentData->type === 'object') { $parentPath = explode('.', $dotPath); $property = array_pop($parentPath); $parentPath = implode('.', $parentPath); $exampleFromParent = Arr::get($results, $dotPath) ?? $parentData->example[$property] ?? null; if (empty($exampleFromParent)) { Arr::set($results, $dotPath, $value); } } else if ($parentData->type === 'object[]') { // When the body is an array, param names will be "[].paramname", so dot paths won't work correctly with "[]" if (Str::startsWith($path, '[].')) { $valueDotPath = substr($dotPath, 3); // Remove initial '.0.' if (isset($results['[]'][0]) && !Arr::has($results['[]'][0], $valueDotPath)) { Arr::set($results['[]'][0], $valueDotPath, $value); } } else { $parentPath = explode('.', $dotPath); $index = (int)array_pop($parentPath); $parentPath = implode('.', $parentPath); $exampleFromParent = Arr::get($results, $dotPath) ?? $parentData->example[$index] ?? null; if (empty($exampleFromParent)) { Arr::set($results, $dotPath, $value); } } } } } public function addAuthField(ExtractedEndpointData $endpointData): void { $isApiAuthed = $this->config->get('auth.enabled', false); if (!$isApiAuthed || !$endpointData->metadata->authenticated) { return; } $strategy = $this->config->get('auth.in'); $parameterName = $this->config->get('auth.name'); $faker = Factory::create(); if ($seed = $this->config->get('examples.faker_seed')) { $faker->seed($seed); } $token = $faker->shuffleString('abcdefghkvaZVDPE1864563'); $valueToUse = $this->config->get('auth.use_value'); $valueToDisplay = $this->config->get('auth.placeholder'); switch ($strategy) { case 'query': case 'query_or_body': $endpointData->auth = ["queryParameters", $parameterName, $valueToUse ?: $token]; $endpointData->queryParameters[$parameterName] = new Parameter([ 'name' => $parameterName, 'type' => 'string', 'example' => $valueToDisplay ?: $token, 'description' => 'Authentication key.', 'required' => true, ]); return; case 'body': $endpointData->auth = ["bodyParameters", $parameterName, $valueToUse ?: $token]; $endpointData->bodyParameters[$parameterName] = new Parameter([ 'name' => $parameterName, 'type' => 'string', 'example' => $valueToDisplay ?: $token, 'description' => 'Authentication key.', 'required' => true, ]); return; case 'bearer': $endpointData->auth = ["headers", "Authorization", "Bearer " . ($valueToUse ?: $token)]; $endpointData->headers['Authorization'] = "Bearer " . ($valueToDisplay ?: $token); return; case 'basic': $endpointData->auth = ["headers", "Authorization", "Basic " . ($valueToUse ?: base64_encode($token))]; $endpointData->headers['Authorization'] = "Basic " . ($valueToDisplay ?: base64_encode($token)); return; case 'header': $endpointData->auth = ["headers", $parameterName, $valueToUse ?: $token]; $endpointData->headers[$parameterName] = $valueToDisplay ?: $token; return; } } protected static function convertStringValueToUploadedFileInstance(string $filePath): UploadedFile { $fileName = basename($filePath); return new File($fileName, fopen($filePath, 'r')); } protected function mergeInheritedMethodsData(string $stage, ExtractedEndpointData $endpointData, array $inheritedDocsOverrides = []): void { $overrides = $inheritedDocsOverrides[$stage] ?? []; $normalizeParamData = fn($data, $key) => array_merge($data, ["name" => $key]); if (is_array($overrides)) { foreach ($overrides as $key => $item) { switch ($stage) { case "responses": $endpointData->responses->concat($overrides); $endpointData->responses->sortBy('status'); break; case "urlParameters": case "bodyParameters": case "queryParameters": $endpointData->$stage[$key] = Parameter::make($normalizeParamData($item, $key)); break; case "responseFields": $endpointData->$stage[$key] = ResponseField::make($normalizeParamData($item, $key)); break; default: $endpointData->$stage[$key] = $item; } } } else if (is_callable($overrides)) { $results = $overrides($endpointData); $endpointData->$stage = match ($stage) { "responses" => ResponseCollection::make($results), "urlParameters", "bodyParameters", "queryParameters" => collect($results)->map(fn($param, $name) => Parameter::make($normalizeParamData($param, $name)))->all(), "responseFields" => collect($results)->map(fn($field, $name) => ResponseField::make($normalizeParamData($field, $name)))->all(), default => $results, }; } } public static function transformOldRouteRulesIntoNewSettings($stage, $rulesToApply, $strategyName, $strategySettings = []) { if ($stage == 'headers' && Str::is('*RouteRules*', $strategyName)) { return $rulesToApply; } if ($stage == 'responses' && Str::is('*ResponseCalls*', $strategyName) &&!empty($rulesToApply['response_calls'])) { // Transform `methods` to `only` $strategySettings = []; if (isset($rulesToApply['response_calls']['methods'])) { if(empty($rulesToApply['response_calls']['methods'])) { // This means all routes are forbidden $strategySettings['except'] = ["*"]; } else { $strategySettings['only'] = array_map( fn($method) => "$method *", $rulesToApply['response_calls']['methods'] ); } } // Copy all other keys over as is foreach (array_keys($rulesToApply['response_calls']) as $key) { if ($key == 'methods') continue; $strategySettings[$key] = $rulesToApply['response_calls'][$key]; } } return $strategySettings; } }