diff --git a/Annotations/AnnotationGenerator.php b/Annotations/AnnotationGenerator.php index b3103f8..f8138ae 100644 --- a/Annotations/AnnotationGenerator.php +++ b/Annotations/AnnotationGenerator.php @@ -104,6 +104,11 @@ class AnnotationGenerator */ protected $parameterExamples; + /** + * @var array + */ + protected $currentTypeAliases; + public function __construct( DocumentationGenerator $generator, ?PathResolver $pathResolver = null, @@ -116,6 +121,7 @@ public function __construct( $this->missingImportantDataWarnings = []; $this->allowLocalRequests = $allowLocalRequests; $this->parameterExamples = null; + $this->currentTypeAliases = []; $this->currentPluginDir = Manager::getInstance()::getPluginDirectory('OpenApiDocs'); } @@ -288,6 +294,7 @@ protected function buildAnnotationForMethod(array $rules, string $pluginName, \R $pluginName, $methodName ); + $this->currentTypeAliases = $this->getTypeAliasesFromClassDocBlock($reflectionMethod->getDeclaringClass()); $params = $this->determineParameters($rules, $pluginName, $methodName, $reflectionMethod); $responses = $this->determineResponses($rules, $pluginName, $methodName, $reflectionMethod, $params); @@ -295,6 +302,7 @@ protected function buildAnnotationForMethod(array $rules, string $pluginName, \R $isPost = !empty($rules['plugins'][$pluginName]['methodsRequiringPost']) && in_array($methodName, $rules['plugins'][$pluginName]['methodsRequiringPost']); + $isPost = $isPost || !empty($params['body']); return $this->compileOperationLines($path, $opId, $pluginName, $params, $responses, $description, $isPost); } @@ -339,6 +347,7 @@ public function getParamInfoFromDocBlock(string $docBlock): array { $factory = DocBlockFactory::createInstance(); $docBlockObject = $factory->create($docBlock); + $rawParamTypes = $this->extractRawParamTypesFromDocBlock($docBlock); $params = []; foreach ($docBlockObject->getTagsByName('param') as $param) { @@ -347,7 +356,7 @@ public function getParamInfoFromDocBlock(string $docBlock): array } $name = ltrim($param->getVariableName(), '$'); $params[$name] = [ - 'type' => (string) $param->getType(), + 'type' => $rawParamTypes[$name] ?? (string) $param->getType(), // Normalise the description. E.g. remove linebreaks and indentation 'description' => trim(preg_replace(['/^\h+/m', '/\R+/u',], ['', ' '], (string) $param->getDescription())), 'byRef' => $param->isReference(), @@ -357,6 +366,30 @@ public function getParamInfoFromDocBlock(string $docBlock): array return $params; } + /** + * Preserve raw @param type text so phpstan aliases and array-shape syntax are not normalised away before + * downstream alias expansion and schema generation. + * + * @return array + */ + protected function extractRawParamTypesFromDocBlock(string $docBlock): array + { + preg_match_all('/@param\s+(.+?)\s+\$([A-Za-z_][A-Za-z0-9_]*)/m', $docBlock, $matches, PREG_SET_ORDER); + + $paramTypes = []; + foreach ($matches as $match) { + $type = trim($match[1]); + $name = trim($match[2]); + if ($type === '' || $name === '') { + continue; + } + + $paramTypes[$name] = $type; + } + + return $paramTypes; + } + /** * Try to extract the response-type of a method from the doc block string. * @@ -456,41 +489,11 @@ public function buildParameterAnnotationData(string $methodName, string $paramNa if (empty($docType)) { $this->addMissingImportantDataWarning($methodName, $paramName, 'Type is not specified in comment block.'); } - $metaType = strtolower(trim($paramMetadata['type'] ?? $docType)); - $type = in_array($metaType, ['string', 'bool']) && !empty($docType) && $docType !== $metaType ? $docType : $metaType; - // Sometimes, doc-block can wrap type hinting with parenthesis. Remove them. - $type = trim($type, '()'); - // If the signature type is array, but the type hinting provides more, use that instead - if ($type === 'array' && $this->hasSpecificArrayShape($docType) && strpos($docType, '|') === false) { - $type = $docType; - } - $typesMap = []; - // Check for pipes and try to list possible types - $typeHints = array_map(function ($typeHint) { - return trim($typeHint); - }, explode('|', $type)); - // If there's more than 1 type hinted and one is bool, remove bool. This is because many params default to false regardless of expected type - if (count($typeHints) > 1 && in_array('bool', $typeHints)) { - $typeHints = array_diff($typeHints, ['bool']); - } - + $type = $this->resolveEffectiveParameterType($paramMetadata, $paramDocInfo); $isRequired = !key_exists('default', $paramMetadata) || $paramMetadata['default'] instanceof NoDefaultValue; - $allTypeHintsAreStringLiterals = $this->areAllTypeHintsStringLiterals($typeHints); - $enumValues = []; - if ($allTypeHintsAreStringLiterals) { - $typesMap['string'] = null; - foreach ($typeHints as $typeHint) { - $enumValues[] = trim(trim($typeHint), '\'"'); - } - } else { - foreach ($typeHints as $typePart) { - $typePart = trim($typePart, ' ()'); - $normalisedType = $this->hasSpecificArrayShape($typePart) ? 'array' : $this->getOpenApiTypeFromPhpType($typePart); - // If the type is array, check if there's a subType - $subType = $this->getArraySubTypeFromPhpType($typePart, $normalisedType); - $typesMap[$normalisedType] = $subType !== null ? $this->getOpenApiTypeFromPhpType($subType) : $subType; - } - } + $typeMetadata = $this->getParameterTypeMetadata($type); + $typesMap = $typeMetadata['types']; + $enumValues = $typeMetadata['enum']; $description = $paramDocInfo['description'] ?? ''; if (empty($description)) { @@ -542,6 +545,74 @@ public function buildParameterAnnotationData(string $methodName, string $paramNa return $paramData; } + protected function buildResolvedParameterAnnotationData( + string $methodName, + string $paramName, + array $paramMetadata, + array $paramDocInfo + ): array { + $paramData = $this->buildParameterAnnotationData($methodName, $paramName, $paramMetadata, $paramDocInfo); + $effectiveType = $this->resolveEffectiveParameterType($paramMetadata, $paramDocInfo); + $configuredExample = $this->getConfiguredParameterExampleValue($paramName, $effectiveType); + + $paramData['_docType'] = $this->expandTypeAliases($effectiveType); + $paramData['_configExample'] = $configuredExample['found'] ? $configuredExample['value'] : null; + $paramData['_isComplex'] = $this->isComplexParameter($paramData); + if ($paramData['_isComplex']) { + $paramData['_schemaDefinition'] = $this->buildRequestBodySchemaDefinition( + strval($paramData['_docType']), + $paramData['types'] ?? [], + $paramData['_configExample'] + ); + } + + return $paramData; + } + + /** + * Parse the raw type string into the normalized OpenAPI type map and enum values. + * + * @param string $type + * + * @return array{types: array, enum: array} + */ + protected function getParameterTypeMetadata(string $type): array + { + $typesMap = []; + $enumValues = []; + + $typeHints = array_map(static function ($typeHint) { + return trim($typeHint); + }, explode('|', $type)); + if (count($typeHints) > 1 && in_array('bool', $typeHints, true)) { + $typeHints = array_values(array_diff($typeHints, ['bool'])); + } + + if ($this->areAllTypeHintsStringLiterals($typeHints)) { + $typesMap['string'] = null; + foreach ($typeHints as $typeHint) { + $enumValues[] = trim(trim($typeHint), '\'"'); + } + + return [ + 'types' => $typesMap, + 'enum' => $enumValues, + ]; + } + + foreach ($typeHints as $typePart) { + $typePart = trim($typePart, ' ()'); + $normalisedType = $this->hasSpecificArrayShape($typePart) ? 'array' : $this->getOpenApiTypeFromPhpType($typePart); + $subType = $this->getArraySubTypeFromPhpType($typePart, $normalisedType); + $typesMap[$normalisedType] = $subType !== null ? $this->getOpenApiTypeFromPhpType($subType) : $subType; + } + + return [ + 'types' => $typesMap, + 'enum' => $enumValues, + ]; + } + /** * Determine whether all type hints are quoted string literals. * @@ -569,7 +640,29 @@ protected function areAllTypeHintsStringLiterals(array $typeHints): bool protected function hasSpecificArrayShape(string $type): bool { - return strpos($type, '[]') !== false || preg_match('/^(array|list)<.+>$/', trim($type)) === 1; + $type = trim($type); + return strpos($type, '[]') !== false + || preg_match('/^(array|list)<.+>$/', $type) === 1 + || preg_match('/^array\s*\{.+\}$/', $type) === 1; + } + + protected function resolveEffectiveParameterType(array $paramMetadata, array $paramDocInfo): string + { + $docType = trim($paramDocInfo['type'] ?? ''); + $metaType = trim($paramMetadata['type'] ?? $docType); + $docTypeNormalised = strtolower($docType); + $metaTypeNormalised = strtolower($metaType); + $type = in_array($metaTypeNormalised, ['string', 'bool'], true) + && $docType !== '' + && $docTypeNormalised !== $metaTypeNormalised + ? $docType + : $metaType; + $type = trim($type, '()'); + if (strtolower($type) === 'array' && $this->hasSpecificArrayShape($docType) && strpos($docType, '|') === false) { + $type = $docType; + } + + return $type; } protected function getArraySubTypeFromPhpType(string $typePart, string $normalisedType): ?string @@ -594,6 +687,25 @@ protected function getArraySubTypeFromPhpType(string $typePart, string $normalis return $genericParts[1]; } + protected function getRawArraySubType(string $typePart): ?string + { + $typePart = trim($typePart); + if (strpos($typePart, '[]') !== false) { + return substr($typePart, 0, strpos($typePart, '[]')); + } + + if (preg_match('/^(array|list)<(.+)>$/', $typePart, $matches) !== 1) { + return null; + } + + $genericParts = $this->splitTopLevel($matches[2], ','); + if (count($genericParts) === 1) { + return trim($genericParts[0]); + } + + return trim($genericParts[1]); + } + /** * Load and return the configured parameter examples. * @@ -618,9 +730,9 @@ protected function getParameterExamplesConfig(): array } /** - * Return a config-backed example string if the configured example is intentionally simple enough to support. + * @return array{found: bool, value: mixed} */ - protected function getParameterExampleFromConfig(string $paramName, string $type, array $typesMap): ?string + protected function getConfiguredParameterExampleValue(string $paramName, string $type): array { $config = $this->getParameterExamplesConfig(); $keysToTry = [ @@ -628,23 +740,34 @@ protected function getParameterExampleFromConfig(string $paramName, string $type $paramName . ':' . preg_replace('/\s+/', '', $type), ]; - $configValue = null; - $foundConfigValue = false; foreach (array_unique($keysToTry) as $key) { if (!array_key_exists($key, $config)) { continue; } - $configValue = $config[$key]; - $foundConfigValue = true; - break; + return [ + 'found' => true, + 'value' => $config[$key], + ]; } - if (!$foundConfigValue) { + return [ + 'found' => false, + 'value' => null, + ]; + } + + /** + * Return a config-backed example string if the configured example is intentionally simple enough to support. + */ + protected function getParameterExampleFromConfig(string $paramName, string $type, array $typesMap): ?string + { + $configuredExample = $this->getConfiguredParameterExampleValue($paramName, $type); + if (!$configuredExample['found']) { return null; } - return $this->normaliseConfiguredParameterExample($configValue, $typesMap); + return $this->normaliseConfiguredParameterExample($configuredExample['value'], $typesMap); } /** @@ -663,7 +786,7 @@ protected function normaliseConfiguredParameterExample($example, array $typesMap if ( !is_array($example) || !$this->isBasicExampleArray($example) - || !$this->supportsBasicArrayExample($typesMap) + || !array_key_exists('array', $typesMap) ) { return null; } @@ -673,12 +796,130 @@ protected function normaliseConfiguredParameterExample($example, array $typesMap return is_string($encoded) ? $encoded : null; } + protected function isComplexParameter(array $param): bool + { + $typesMap = $param['types'] ?? []; + if (!array_key_exists('array', $typesMap)) { + return false; + } + + $docType = trim(strval($param['_docType'] ?? '')); + if ($docType !== '') { + $typeHints = array_map('trim', explode('|', $docType)); + $hasAmbiguousArray = false; + foreach ($typeHints as $typeHint) { + $typeHint = trim($typeHint, ' ()'); + if ($this->isInlineArrayShapeType($typeHint)) { + return true; + } + + if (!$this->hasSpecificArrayShape($typeHint) && strtolower($typeHint) !== 'array') { + continue; + } + + if (strtolower($typeHint) === 'array') { + $hasAmbiguousArray = true; + continue; + } + + $rawSubType = $this->getRawArraySubType($typeHint); + if ($rawSubType === null || $rawSubType === '') { + $hasAmbiguousArray = true; + continue; + } + + if ($this->isInlineArrayShapeType($rawSubType)) { + return true; + } + + $rawSubType = strtolower(trim($rawSubType)); + if ($this->isScalarPhpType($rawSubType)) { + continue; + } + + return true; + } + + if ($hasAmbiguousArray) { + return $this->isComplexArrayExample($param['_configExample'] ?? null); + } + + return false; + } + + $arraySubType = $typesMap['array']; + if (is_string($arraySubType) && $this->isScalarOpenApiType($arraySubType)) { + return false; + } + + return $this->isComplexArrayExample($param['_configExample'] ?? null); + } + + protected function isComplexArrayExample($example): bool + { + if (!is_array($example)) { + return true; + } + + return !$this->isBasicExampleArray($example); + } + + protected function isScalarPhpType(string $type): bool + { + return in_array($type, ['string', 'int', 'integer', 'float', 'double', 'bool', 'boolean', 'number'], true); + } + + protected function isScalarOpenApiType(string $type): bool + { + return in_array($type, ['string', 'integer', 'number', 'boolean'], true); + } + + protected function isInlineArrayShapeType(string $type): bool + { + return preg_match('/array\s*\{/', trim($type)) === 1; + } + /** - * Only use array config examples when the emitted schema includes an array shape. + * @return array */ - protected function supportsBasicArrayExample(array $typesMap): bool + protected function getTypeAliasesFromClassDocBlock(\ReflectionClass $reflectionClass): array + { + $docBlock = $reflectionClass->getDocComment(); + if ($docBlock === false || $docBlock === '') { + return []; + } + + preg_match_all('/@phpstan-type\s+([A-Za-z_][A-Za-z0-9_]*)\s+([^\n\r*]+)/', $docBlock, $matches, PREG_SET_ORDER); + $aliases = []; + foreach ($matches as $match) { + $aliasName = trim($match[1]); + $aliasType = trim($match[2]); + if ($aliasName === '' || $aliasType === '') { + continue; + } + + $aliases[$aliasName] = $aliasType; + } + + return $aliases; + } + + protected function expandTypeAliases(string $type): string { - return array_key_exists('array', $typesMap); + if ($type === '' || empty($this->currentTypeAliases)) { + return $type; + } + + $expandedType = $type; + foreach ($this->currentTypeAliases as $aliasName => $aliasType) { + $expandedType = preg_replace( + '/(?getParamInfoFromDocBlock($docBlock); } - $customParams = []; foreach ($paramsMetadata as $name => $paramMetadata) { $paramInfo = $paramsInfo[$name] ?? []; // Skip references and variadic for now @@ -812,7 +1055,7 @@ protected function determineParameters(array $rules, string $plugin, string $met } // If the parameter doesn't have a description and matches a global, use a reference to the global instead. - $customParamData = $this->buildParameterAnnotationData($method, $name, $paramMetadata, $paramInfo); + $customParamData = $this->buildResolvedParameterAnnotationData($method, $name, $paramMetadata, $paramInfo); if (empty($customParamData['description']) && in_array($name, self::GLOBAL_PARAMETER_NAMES)) { $globalParamSuffix = $customParamData['required'] === 'true' ? 'Required' : 'Optional'; $paramRef = '#/components/parameters/' . $name . $globalParamSuffix; @@ -826,11 +1069,18 @@ protected function determineParameters(array $rules, string $plugin, string $met } $customParams[] = $customParamData; + if ($customParamData['_isComplex']) { + $bodyParams[] = $customParamData; + } else { + $queryParams[] = $customParamData; + } } return [ 'refs' => array_values(array_unique($refs)), 'custom' => $customParams, + 'query' => $queryParams, + 'body' => $bodyParams, ]; } @@ -940,8 +1190,9 @@ protected function getApplicableDemoExampleUrls(string $pluginName, string $meth } $parametersToReplace = []; - if (!empty($paramsData['custom'])) { - foreach ($paramsData['custom'] as $customParam) { + $queryParams = $paramsData['query'] ?? ($paramsData['custom'] ?? []); + if (!empty($queryParams)) { + foreach ($queryParams as $customParam) { // Skip any which might be references. if (!is_array($customParam)) { continue; @@ -2072,6 +2323,351 @@ public function buildSchemaObjectArrays(array $typesMap, string $default = '', s return ['@OA\Schema' => ['oneOf={' => $schemas]]; } + protected function buildRequestBodyAnnotation(array $bodyParams): array + { + $requiredParamNames = []; + $schemaProperties = ['type="object"']; + $requestBodyExample = []; + foreach ($bodyParams as $param) { + if (($param['required'] ?? 'false') === 'true') { + $requiredParamNames[] = '"' . $param['name'] . '"'; + } + + $schemaProperties[] = $this->buildRequestBodyProperty($param); + if (($param['required'] ?? 'false') === 'true' && array_key_exists('_configExample', $param) && $param['_configExample'] !== null) { + $requestBodyExample[$param['name']] = $param['_configExample']; + } + } + + if (!empty($requiredParamNames)) { + $schemaProperties[] = 'required={' . implode(',', $requiredParamNames) . '}'; + } + + $mediaTypeProperties = [ + 'mediaType="application/x-www-form-urlencoded"', + '@OA\Schema' => $schemaProperties, + ]; + if (!empty($requestBodyExample)) { + $mediaTypeProperties[] = 'example=' . $this->buildAnnotationLiteralFromValue($requestBodyExample); + } + + return [ + '@OA\RequestBody' => [ + 'required=' . (!empty($requiredParamNames) ? 'true' : 'false'), + '@OA\MediaType' => $mediaTypeProperties, + ], + ]; + } + + protected function buildRequestBodyProperty(array $param): array + { + $propertyLines = [ + 'property="' . $param['name'] . '"', + ]; + if (!empty($param['description'])) { + $propertyLines[] = 'description="' . $param['description'] . '"'; + } + + $schemaDefinition = $param['_schemaDefinition'] ?? $this->buildRequestBodySchemaDefinition( + strval($param['_docType'] ?? ''), + $param['types'] ?? [], + $param['_configExample'] ?? null + ); + $propertyLines = array_merge($propertyLines, $this->buildSchemaLinesFromDefinition($schemaDefinition)); + + if (($param['required'] ?? 'false') === 'true' && array_key_exists('_configExample', $param) && $param['_configExample'] !== null) { + $propertyLines[] = 'example=' . $this->buildAnnotationLiteralFromValue($param['_configExample']); + } + + return ['@OA\Property' => $propertyLines]; + } + + protected function buildRequestBodySchemaDefinition(string $type, array $typesMap, $example = null): array + { + $type = trim($type); + if ($this->isInlineArrayShapeType($type)) { + return $this->parseShapeTypeDefinition($type); + } + + $arrayDefinition = $this->parseArrayLikeTypeDefinition($type); + if ($arrayDefinition !== null) { + return $arrayDefinition; + } + + if (strtolower($type) === 'array') { + return [ + 'type' => 'object', + 'additionalProperties' => true, + ]; + } + + if (is_array($example)) { + if (array_values($example) !== $example) { + return [ + 'type' => 'object', + 'additionalProperties' => true, + ]; + } + + if (!empty($example) && is_array(reset($example))) { + return [ + 'type' => 'array', + 'items' => [ + 'type' => 'object', + 'additionalProperties' => true, + ], + ]; + } + } + + if (array_key_exists('array', $typesMap)) { + return [ + 'type' => 'array', + 'items' => [ + 'type' => 'object', + 'additionalProperties' => true, + ], + ]; + } + + return [ + 'type' => 'object', + 'additionalProperties' => true, + ]; + } + + protected function parseShapeTypeDefinition(string $type): array + { + $type = trim($type); + $arrayDefinition = $this->parseArrayLikeTypeDefinition($type); + if ($arrayDefinition !== null) { + return $arrayDefinition; + } + + if (preg_match('/^array\s*\{(.+)\}$/', $type, $matches) === 1) { + $properties = []; + $required = []; + foreach ($this->splitTopLevel($matches[1], ',') as $fieldDefinition) { + [$name, $fieldType] = array_pad($this->splitTopLevel($fieldDefinition, ':'), 2, ''); + $name = trim($name); + $isRequired = !str_ends_with($name, '?'); + $name = rtrim($name, '?'); + if ($name === '' || trim($fieldType) === '') { + continue; + } + + if ($isRequired) { + $required[] = $name; + } + + $properties[] = [ + 'name' => $name, + 'schema' => $this->parseShapeTypeDefinition(trim($fieldType)), + ]; + } + + return [ + 'type' => 'object', + 'properties' => $properties, + 'required' => $required, + ]; + } + + $normalisedType = $this->getOpenApiTypeFromPhpType($type); + if ($this->isScalarOpenApiType($normalisedType)) { + return [ + 'type' => $normalisedType, + ]; + } + + if (strtolower($type) === 'mixed') { + return [ + 'type' => 'object', + 'additionalProperties' => true, + ]; + } + + return [ + 'type' => 'object', + 'additionalProperties' => true, + ]; + } + + protected function parseArrayLikeTypeDefinition(string $type): ?array + { + $type = trim($type); + $arrayLikeType = $this->extractArrayLikeValueType($type); + if ($arrayLikeType === null) { + return null; + } + + if ($arrayLikeType['keyType'] === 'string') { + return $this->buildStringKeyedArraySchemaDefinition($arrayLikeType['valueType']); + } + + return [ + 'type' => 'array', + 'items' => $this->buildArrayItemSchemaDefinition($arrayLikeType['valueType']), + ]; + } + + /** + * @return array{keyType: string|null, valueType: string}|null + */ + protected function extractArrayLikeValueType(string $type): ?array + { + if (preg_match('/^(array|list)<(.+)>$/', $type, $matches) === 1) { + $genericParts = $this->splitTopLevel($matches[2], ','); + if (empty($genericParts)) { + return null; + } + + $keyType = null; + $valueType = trim(end($genericParts)); + if (count($genericParts) === 2) { + $keyType = strtolower(trim($genericParts[0])); + } + + return [ + 'keyType' => $keyType, + 'valueType' => $valueType, + ]; + } + + if (strpos($type, '[]') === false) { + return null; + } + + return [ + 'keyType' => null, + 'valueType' => trim(substr($type, 0, strpos($type, '[]'))), + ]; + } + + protected function buildStringKeyedArraySchemaDefinition(string $valueType): array + { + if ($this->isInlineArrayShapeType($valueType)) { + return $this->parseShapeTypeDefinition($valueType); + } + + return [ + 'type' => 'object', + 'additionalProperties' => true, + ]; + } + + protected function buildArrayItemSchemaDefinition(string $valueType): array + { + if ($this->isInlineArrayShapeType($valueType)) { + return $this->parseShapeTypeDefinition($valueType); + } + + if ($this->isScalarPhpType(strtolower($valueType))) { + return [ + 'type' => $this->getOpenApiTypeFromPhpType($valueType), + ]; + } + + return [ + 'type' => 'object', + 'additionalProperties' => true, + ]; + } + + protected function buildSchemaLinesFromDefinition(array $definition): array + { + $schemaLines = []; + if (!empty($definition['type'])) { + $schemaLines[] = 'type="' . $definition['type'] . '"'; + } + if (!empty($definition['additionalProperties'])) { + $schemaLines[] = 'additionalProperties=true'; + } + if (!empty($definition['required'])) { + $requiredProperties = array_map(static function ($propertyName) { + return '"' . $propertyName . '"'; + }, $definition['required']); + $schemaLines[] = 'required={' . implode(',', $requiredProperties) . '}'; + } + if (!empty($definition['items']) && is_array($definition['items'])) { + $schemaLines[] = ['@OA\Items' => $this->buildSchemaLinesFromDefinition($definition['items'])]; + } + if (!empty($definition['properties']) && is_array($definition['properties'])) { + foreach ($definition['properties'] as $property) { + $schemaLines[] = ['@OA\Property' => array_merge( + ['property="' . $property['name'] . '"'], + $this->buildSchemaLinesFromDefinition($property['schema']) + )]; + } + } + + return $schemaLines; + } + + protected function buildAnnotationLiteralFromValue($value): string + { + if (is_bool($value)) { + return $value ? 'true' : 'false'; + } + + if (is_int($value) || is_float($value)) { + return strval($value); + } + + if (is_string($value)) { + return '"' . str_replace('"', '\"', $value) . '"'; + } + + if (is_array($value)) { + $encoded = json_encode($value); + if (!is_string($encoded)) { + return '{}'; + } + + return str_replace(['[', ']'], ['{', '}'], $encoded); + } + + return '{}'; + } + + /** + * @return string[] + */ + protected function splitTopLevel(string $value, string $delimiter): array + { + $parts = []; + $current = ''; + $braceDepth = 0; + $angleDepth = 0; + $length = strlen($value); + for ($index = 0; $index < $length; $index++) { + $character = $value[$index]; + if ($character === '{') { + $braceDepth++; + } elseif ($character === '}') { + $braceDepth--; + } elseif ($character === '<') { + $angleDepth++; + } elseif ($character === '>') { + $angleDepth--; + } + + if ($character === $delimiter && $braceDepth === 0 && $angleDepth === 0) { + $parts[] = trim($current); + $current = ''; + continue; + } + + $current .= $character; + } + + if ($current !== '') { + $parts[] = trim($current); + } + + return $parts; + } + /** * Build the full array of lines for an OA operation. E.g. OA\Get or OA\Post * @@ -2097,7 +2693,22 @@ public function compileOperationLines(string $path, string $opId, string $plugin foreach ($params['refs'] ?? [] as $ref) { $operationValuesMap[] = '@OA\Parameter(ref="' . $ref . '")'; } - foreach ($params['custom'] ?? [] as $param) { + $queryParams = $params['query'] ?? null; + $bodyParams = $params['body'] ?? null; + if ($queryParams === null || $bodyParams === null) { + $queryParams = []; + $bodyParams = []; + foreach ($params['custom'] ?? [] as $param) { + if (is_array($param) && !empty($param['_isComplex'])) { + $bodyParams[] = $param; + continue; + } + + $queryParams[] = $param; + } + } + + foreach ($queryParams as $param) { if (!is_array($param)) { if (!is_string($param) || stripos($param, '#/components/parameters/') === false) { throw new \Exception('Invalid custom param: ' . strval($param)); @@ -2107,6 +2718,11 @@ public function compileOperationLines(string $path, string $opId, string $plugin continue; } + if (!empty($param['_isComplex'])) { + $bodyParams[] = $param; + continue; + } + $paramMap = [ 'name="' . $param['name'] . '"', 'in="query"', @@ -2135,6 +2751,11 @@ public function compileOperationLines(string $path, string $opId, string $plugin ); $operationValuesMap[] = ['@OA\Parameter' => $paramMap]; } + if (!empty($bodyParams)) { + $operationValuesMap[] = $this->buildRequestBodyAnnotation($bodyParams); + } + $isPost = $isPost || !empty($bodyParams); + $operationName = '@OA\\' . ($isPost ? 'Post' : 'Get'); foreach ($responses as $response) { $responseDescription = $this->getDescriptionText($response['description'] ?? null); @@ -2162,7 +2783,7 @@ public function compileOperationLines(string $path, string $opId, string $plugin } } - $lines = $this->buildLinesForAnnotationObject('@OA\\' . ($isPost ? 'Post' : 'Get'), $operationValuesMap); + $lines = $this->buildLinesForAnnotationObject($operationName, $operationValuesMap); // Trim the comma off the very last item at this level and return the array $this->removeTrailingCommaFromLastLine($lines); diff --git a/config/ParameterExamples.php b/config/ParameterExamples.php index 2a4452f..09ea743 100644 --- a/config/ParameterExamples.php +++ b/config/ParameterExamples.php @@ -157,8 +157,8 @@ 'format:string' => 'html', 'visits:array' => [ [ - 'idVisit' => 12345, - 'idSite' => 1, + 'idvisit' => 12345, + 'idsite' => 1, ], ], 'segment:string' => 'countryCode==NZ', diff --git a/tests/Resources/MockAnnotationGenerator.php b/tests/Resources/MockAnnotationGenerator.php index 23e4c6e..6ee5659 100644 --- a/tests/Resources/MockAnnotationGenerator.php +++ b/tests/Resources/MockAnnotationGenerator.php @@ -19,95 +19,40 @@ class MockAnnotationGenerator extends AnnotationGenerator public function __construct(DocumentationGenerator $generator) { parent::__construct($generator); - - // TODO - Extend the constructor behaviour } - // TODO - Refactor the methods below to use dependency injection so that they can more easily be tested - - /** - * @inheritDoc - */ - public function buildAnnotationForMethod(array $rules, string $pluginName, \ReflectionMethod $reflectionMethod): array - { - return parent::buildAnnotationForMethod($rules, $pluginName, $reflectionMethod); - } - - /** - * @inheritDoc - */ - public function determineParameters(array $rules, string $plugin, string $method, \ReflectionMethod $reflectionMethod): array + public function shouldUseParameterLevelExample(array $typesMap, string $example): bool { - return parent::determineParameters($rules, $plugin, $method, $reflectionMethod); + return parent::shouldUseParameterLevelExample($typesMap, $example); } - /** - * @inheritDoc - */ public function getApplicableDemoExampleUrls(string $pluginName, string $methodName, array $paramsData): array { return parent::getApplicableDemoExampleUrls($pluginName, $methodName, $paramsData); } - /** - * @inheritDoc - */ - public function getDemoReportMetadata(): array - { - return parent::getDemoReportMetadata(); - } - - /** - * @inheritDoc - */ - public function getExampleIfAvailable(string $url, bool $useLocalToken = false, bool $ignoreCached = false): string - { - return parent::getExampleIfAvailable($url, $useLocalToken, $ignoreCached); - } - - /** - * @inheritDoc - */ - public function getReportExampleUrlFromMetadata(string $pluginName, string $methodName): string - { - return parent::getReportExampleUrlFromMetadata($pluginName, $methodName); - } - public function getReportMetadataUrl(): string { return parent::getReportMetadataUrl(); } - public function prependInstanceUrl(string $path): string - { - return parent::prependInstanceUrl($path); - } - - /** - * @inheritDoc - */ - public function determineResponses(array $rules, string $plugin, string $method, \ReflectionMethod $reflectionMethod, array $paramsData): array - { - return parent::determineResponses($rules, $plugin, $method, $reflectionMethod, $paramsData); - } - - public function normaliseConfiguredParameterExample($example, array $typesMap = []): ?string + public function getReportExampleUrlFromMetadata(string $pluginName, string $methodName): string { - return parent::normaliseConfiguredParameterExample($example, $typesMap); + return parent::getReportExampleUrlFromMetadata($pluginName, $methodName); } - public function isBasicExampleArray(array $example): bool + public function expandTypeAliases(string $type): string { - return parent::isBasicExampleArray($example); + return parent::expandTypeAliases($type); } - public function supportsBasicArrayExample(array $typesMap): bool + public function parseArrayLikeTypeDefinition(string $type): ?array { - return parent::supportsBasicArrayExample($typesMap); + return parent::parseArrayLikeTypeDefinition($type); } - public function shouldUseParameterLevelExample(array $typesMap, string $example): bool + public function setCurrentTypeAliases(array $aliases): void { - return parent::shouldUseParameterLevelExample($typesMap, $example); + $this->currentTypeAliases = $aliases; } } diff --git a/tests/Unit/AnnotationGeneratorTest.php b/tests/Unit/AnnotationGeneratorTest.php index 77c841a..d2c8aaf 100644 --- a/tests/Unit/AnnotationGeneratorTest.php +++ b/tests/Unit/AnnotationGeneratorTest.php @@ -396,6 +396,20 @@ public function testGetParamInfoFromDocBlock(): void $this->assertEquals($expected, $this->annotationGenerator->getParamInfoFromDocBlock(self::EXAMPLE_API_METHOD_DOC_BLOCK1)); } + public function testGetParamInfoFromDocBlockPreservesRawAliasTypes(): void + { + $docBlock = <<<'DOC' +/** + * @param array $visits Data subject visit descriptors to export. + */ +DOC; + + $this->assertSame( + 'array', + $this->annotationGenerator->getParamInfoFromDocBlock($docBlock)['visits']['type'] + ); + } + public function testGetResponseInfoFromDocBlock(): void { // TODO - Update to use resource file and/or dataprovider to test more than one comment block @@ -1297,17 +1311,16 @@ public function testBuildSchemaObjectArrayIgnoresEnumForNonStringTypes(): void $this->assertEquals($expectedWithoutEnum, $this->annotationGenerator->buildSchemaObjectArray('integer', '', NoDefaultValue::class, '1', ['1', '2'])); } - public function testNormaliseConfiguredParameterExampleSupportsOnlySimpleValues(): void + public function testBuildParameterAnnotationDataUsesConfiguredArrayExample(): void { - $annotationGenerator = new MockAnnotationGenerator(new DocumentationGenerator()); + $result = $this->annotationGenerator->buildParameterAnnotationData( + 'someMethodName', + 'statuses', + [], + ['type' => 'string|array'] + ); - $this->assertSame('true', $annotationGenerator->normaliseConfiguredParameterExample(true)); - $this->assertSame('1.5', $annotationGenerator->normaliseConfiguredParameterExample(1.5)); - $this->assertSame('["one","two"]', $annotationGenerator->normaliseConfiguredParameterExample(['one', 'two'], ['array' => 'string'])); - $this->assertNull($annotationGenerator->normaliseConfiguredParameterExample(['one', 'two'], ['string' => null])); - $this->assertSame('["one","two"]', $annotationGenerator->normaliseConfiguredParameterExample(['one', 'two'], ['array' => 'string', 'string' => null])); - $this->assertNull($annotationGenerator->normaliseConfiguredParameterExample(['key' => 'value'], ['array' => 'string'])); - $this->assertNull($annotationGenerator->normaliseConfiguredParameterExample([['nested']], ['array' => 'string'])); + $this->assertSame('["running","finished"]', $result['example']); } public function testShouldUseParameterLevelExampleForScalarArrayUnions(): void @@ -1410,9 +1423,122 @@ public function testBuildSchemaObjectArrays(): void $this->expectNotToPerformAssertions(); } + public function testExpandTypeAliasesExpandsNamedArrayShapeAliases(): void + { + $annotationGenerator = new MockAnnotationGenerator(new DocumentationGenerator()); + $annotationGenerator->setCurrentTypeAliases([ + 'VisitDescriptor' => 'array{idsite: int, idvisit: int}', + ]); + + $this->assertSame( + 'array', + $annotationGenerator->expandTypeAliases('array') + ); + } + + public function testParseArrayLikeTypeDefinitionReturnsScalarArraySchema(): void + { + $annotationGenerator = new MockAnnotationGenerator(new DocumentationGenerator()); + + $this->assertSame([ + 'type' => 'array', + 'items' => [ + 'type' => 'integer', + ], + ], $annotationGenerator->parseArrayLikeTypeDefinition('list')); + } + + public function testParseArrayLikeTypeDefinitionReturnsArrayShapeItemSchema(): void + { + $annotationGenerator = new MockAnnotationGenerator(new DocumentationGenerator()); + + $this->assertSame([ + 'type' => 'array', + 'items' => [ + 'type' => 'object', + 'properties' => [ + [ + 'name' => 'idsite', + 'schema' => [ + 'type' => 'integer', + ], + ], + ], + 'required' => ['idsite'], + ], + ], $annotationGenerator->parseArrayLikeTypeDefinition('array')); + } + + public function testParseArrayLikeTypeDefinitionReturnsStringKeyedShapeSchema(): void + { + $annotationGenerator = new MockAnnotationGenerator(new DocumentationGenerator()); + + $this->assertSame([ + 'type' => 'object', + 'properties' => [ + [ + 'name' => 'idsite', + 'schema' => [ + 'type' => 'integer', + ], + ], + ], + 'required' => ['idsite'], + ], $annotationGenerator->parseArrayLikeTypeDefinition('array')); + } + public function testCompileOperationLines(): void { - // TODO - compileOperationLines method - $this->expectNotToPerformAssertions(); + $lines = $this->annotationGenerator->compileOperationLines( + '/index.php?module=API&method=PrivacyManager.exportDataSubjects', + 'PrivacyManager.exportDataSubjects', + 'PrivacyManager', + [ + 'refs' => [], + 'custom' => [ + [ + 'name' => 'idSite', + 'types' => ['integer' => null], + 'description' => 'Site ID', + 'required' => 'true', + 'default' => NoDefaultValue::class, + 'example' => '1', + ], + [ + 'name' => 'visits', + 'types' => ['array' => 'string'], + 'description' => 'Visit descriptors.', + 'required' => 'true', + 'default' => NoDefaultValue::class, + 'example' => '', + '_docType' => 'array', + '_configExample' => [ + [ + 'idsite' => 1, + 'idvisit' => 12345, + ], + ], + '_isComplex' => true, + ], + ], + ], + [ + [ + 'code' => 200, + 'description' => 'OK', + 'schema' => ['@OA\Schema' => ['type="array"']], + ], + ], + '', + true + ); + $annotation = implode("\n", $lines); + + $this->assertStringContainsString('@OA\Post(', $annotation); + $this->assertStringContainsString('name="idSite"', $annotation); + $this->assertStringContainsString('in="query"', $annotation); + $this->assertStringContainsString('@OA\RequestBody(', $annotation); + $this->assertStringNotContainsString('name="visits"', $annotation); + $this->assertStringContainsString('property="visits"', $annotation); } } diff --git a/vue/dist/OpenApiDocs.css b/vue/dist/OpenApiDocs.css index a64c263..0a7fc5e 100644 --- a/vue/dist/OpenApiDocs.css +++ b/vue/dist/OpenApiDocs.css @@ -1 +1 @@ -.swaggerLoader[data-v-7e9c9564]{max-height:100px;display:flex;align-items:center;justify-content:center}.swaggerMount[data-v-7e9c9564]{min-height:180px;visibility:hidden}.swaggerMount--ready[data-v-7e9c9564]{visibility:visible}.swaggerMount[data-v-7e9c9564] .swagger-ui{border:0;border-radius:0;color:var(--theme-color-text,#3b4151);font-size:14px;line-height:1.5;padding-top:0}.page[data-v-7d1e8f2e]{color:var(--theme-color-text,#3b4151)}.searchBar[data-v-7d1e8f2e]{position:relative;margin-bottom:1.5rem;width:300px}.searchIcon[data-v-7d1e8f2e]{position:absolute;top:13px;left:12px;color:var(--theme-color-text-lighter,#98a2b3);font-size:14px;pointer-events:none}.searchInput[data-v-7d1e8f2e]{width:100%;height:38px;padding:10px 12px 10px 38px;background:var(--theme-color-background-contrast,#fff);border:1px solid var(--theme-color-border,#d0d5dd);border-radius:8px;color:var(--theme-color-text,#3b4151);font-size:14px;box-shadow:none}.searchInput[data-v-7d1e8f2e]:focus-visible{border:1px solid var(--theme-color-focus-ring,#5b8def);outline:1px solid var(--theme-color-focus-ring,#5b8def)}.searchInput[data-v-7d1e8f2e]::-moz-placeholder{color:var(--theme-color-text-lighter,#98a2b3)}.searchInput[data-v-7d1e8f2e]::-ms-input-placeholder{color:var(--theme-color-text-lighter,#98a2b3)}.searchInput[data-v-7d1e8f2e]::placeholder{color:var(--theme-color-text-lighter,#98a2b3)}.emptyText[data-v-7d1e8f2e]{margin-bottom:0;color:var(--theme-color-text-light,#646464)}.pluginCard[data-v-7d1e8f2e]{background:var(--theme-color-background-contrast,#fff);border:1px solid var(--theme-color-border,#d9e2ec);border-radius:4px;box-shadow:none;overflow:hidden;transition:border-color .18s ease}.pluginCard--expanded[data-v-7d1e8f2e]{border-color:var(--theme-color-border,#cfd8e3);transform-origin:top center}.pluginToggle[data-v-7d1e8f2e]{width:100%;padding:16px 20px;border:0;outline:none;background:var(--theme-color-background-contrast,#fff);display:flex;align-items:center;color:inherit;cursor:pointer;font:inherit;text-align:left}.pluginToggle[data-v-7d1e8f2e]:focus-visible{box-shadow:inset 0 0 0 2px var(--theme-color-focus-ring,#cfd8e3)}.pluginHeader[data-v-7d1e8f2e]{display:flex;align-items:center;gap:12px}.pluginChevron[data-v-7d1e8f2e]{flex:0 0 12px;color:var(--theme-color-text-light,#5b6b7c);font-size:12px;display:inline-flex;align-items:center;justify-content:center}.pluginName[data-v-7d1e8f2e]{font-size:15px;font-weight:500}.pluginBody[data-v-7d1e8f2e]{position:relative;padding:0}.pluginBody[data-v-7d1e8f2e]:before{content:"";position:absolute;top:0;left:20px;right:20px;border-top:1px solid var(--theme-color-border,#e6edf5)} \ No newline at end of file +.swaggerLoader[data-v-49ed2978]{max-height:100px;display:flex;align-items:center;justify-content:center}.swaggerMount[data-v-49ed2978]{min-height:180px;visibility:hidden}.swaggerMount--ready[data-v-49ed2978]{visibility:visible}.swaggerMount[data-v-49ed2978] .swagger-ui{border:0;border-radius:0;color:var(--theme-color-text,#3b4151);font-size:14px;line-height:1.5;padding-top:0}.page[data-v-cadc72c4]{color:var(--theme-color-text,#3b4151)}.searchBar[data-v-cadc72c4]{position:relative;margin-bottom:1.5rem;width:300px}.searchIcon[data-v-cadc72c4]{position:absolute;top:13px;left:12px;color:var(--theme-color-text-lighter,#98a2b3);font-size:14px;pointer-events:none}.searchInput[data-v-cadc72c4]{width:100%;height:38px;padding:10px 12px 10px 38px;background:var(--theme-color-background-contrast,#fff);border:1px solid var(--theme-color-border,#d0d5dd);border-radius:8px;color:var(--theme-color-text,#3b4151);font-size:14px;box-shadow:none}.searchInput[data-v-cadc72c4]:focus-visible{border:1px solid var(--theme-color-focus-ring,#5b8def);outline:1px solid var(--theme-color-focus-ring,#5b8def)}.searchInput[data-v-cadc72c4]::-moz-placeholder{color:var(--theme-color-text-lighter,#98a2b3)}.searchInput[data-v-cadc72c4]::-ms-input-placeholder{color:var(--theme-color-text-lighter,#98a2b3)}.searchInput[data-v-cadc72c4]::placeholder{color:var(--theme-color-text-lighter,#98a2b3)}.emptyText[data-v-cadc72c4]{margin-bottom:0;color:var(--theme-color-text-light,#646464)}.pluginCard[data-v-cadc72c4]{background:var(--theme-color-background-contrast,#fff);border:1px solid var(--theme-color-border,#d9e2ec);border-radius:4px;box-shadow:none;overflow:hidden;transition:border-color .18s ease}.pluginCard--expanded[data-v-cadc72c4]{border-color:var(--theme-color-border,#cfd8e3);transform-origin:top center}.pluginToggle[data-v-cadc72c4]{width:100%;padding:16px 20px;border:0;outline:none;background:var(--theme-color-background-contrast,#fff);display:flex;align-items:center;color:inherit;cursor:pointer;font:inherit;text-align:left}.pluginToggle[data-v-cadc72c4]:focus-visible{box-shadow:inset 0 0 0 2px var(--theme-color-focus-ring,#cfd8e3)}.pluginHeader[data-v-cadc72c4]{display:flex;align-items:center;gap:12px}.pluginChevron[data-v-cadc72c4]{flex:0 0 12px;color:var(--theme-color-text-light,#5b6b7c);font-size:12px;display:inline-flex;align-items:center;justify-content:center}.pluginName[data-v-cadc72c4]{font-size:15px;font-weight:500}.pluginBody[data-v-cadc72c4]{position:relative;padding:0}.pluginBody[data-v-cadc72c4]:before{content:"";position:absolute;top:0;left:20px;right:20px;border-top:1px solid var(--theme-color-border,#e6edf5)} \ No newline at end of file diff --git a/vue/dist/OpenApiDocs.umd.js b/vue/dist/OpenApiDocs.umd.js index eea7923..bbe9df2 100644 --- a/vue/dist/OpenApiDocs.umd.js +++ b/vue/dist/OpenApiDocs.umd.js @@ -87,7 +87,7 @@ return /******/ (function(modules) { // webpackBootstrap /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; /******/ /******/ // __webpack_public_path__ -/******/ __webpack_require__.p = "plugins/OpenApiDocs/vue/dist/"; +/******/ __webpack_require__.p = "plugins/ApiReference/vue/dist/"; /******/ /******/ /******/ // Load entry module and return exports @@ -96,56 +96,56 @@ return /******/ (function(modules) { // webpackBootstrap /************************************************************************/ /******/ ({ -/***/ "003e": -/***/ (function(module, exports, __webpack_require__) { +/***/ "19dc": +/***/ (function(module, exports) { -// extracted by mini-css-extract-plugin +module.exports = __WEBPACK_EXTERNAL_MODULE__19dc__; /***/ }), -/***/ "0edb": +/***/ "4eeb": /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; -/* harmony import */ var _node_modules_vue_cli_service_node_modules_mini_css_extract_plugin_dist_loader_js_ref_7_oneOf_1_0_node_modules_vue_cli_service_node_modules_css_loader_dist_cjs_js_ref_7_oneOf_1_1_node_modules_vue_cli_service_node_modules_vue_loader_v16_dist_stylePostLoader_js_node_modules_postcss_loader_src_index_js_ref_7_oneOf_1_2_node_modules_vue_cli_service_node_modules_cache_loader_dist_cjs_js_ref_1_0_node_modules_vue_cli_service_node_modules_vue_loader_v16_dist_index_js_ref_1_1_SwaggerPage_vue_vue_type_style_index_0_id_7d1e8f2e_scoped_true_lang_css__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("cd14"); -/* harmony import */ var _node_modules_vue_cli_service_node_modules_mini_css_extract_plugin_dist_loader_js_ref_7_oneOf_1_0_node_modules_vue_cli_service_node_modules_css_loader_dist_cjs_js_ref_7_oneOf_1_1_node_modules_vue_cli_service_node_modules_vue_loader_v16_dist_stylePostLoader_js_node_modules_postcss_loader_src_index_js_ref_7_oneOf_1_2_node_modules_vue_cli_service_node_modules_cache_loader_dist_cjs_js_ref_1_0_node_modules_vue_cli_service_node_modules_vue_loader_v16_dist_index_js_ref_1_1_SwaggerPage_vue_vue_type_style_index_0_id_7d1e8f2e_scoped_true_lang_css__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_node_modules_vue_cli_service_node_modules_mini_css_extract_plugin_dist_loader_js_ref_7_oneOf_1_0_node_modules_vue_cli_service_node_modules_css_loader_dist_cjs_js_ref_7_oneOf_1_1_node_modules_vue_cli_service_node_modules_vue_loader_v16_dist_stylePostLoader_js_node_modules_postcss_loader_src_index_js_ref_7_oneOf_1_2_node_modules_vue_cli_service_node_modules_cache_loader_dist_cjs_js_ref_1_0_node_modules_vue_cli_service_node_modules_vue_loader_v16_dist_index_js_ref_1_1_SwaggerPage_vue_vue_type_style_index_0_id_7d1e8f2e_scoped_true_lang_css__WEBPACK_IMPORTED_MODULE_0__); +/* harmony import */ var _node_modules_vue_cli_service_node_modules_mini_css_extract_plugin_dist_loader_js_ref_7_oneOf_1_0_node_modules_vue_cli_service_node_modules_css_loader_dist_cjs_js_ref_7_oneOf_1_1_node_modules_vue_cli_service_node_modules_vue_loader_v16_dist_stylePostLoader_js_node_modules_postcss_loader_src_index_js_ref_7_oneOf_1_2_node_modules_vue_cli_service_node_modules_cache_loader_dist_cjs_js_ref_1_0_node_modules_vue_cli_service_node_modules_vue_loader_v16_dist_index_js_ref_1_1_SwaggerPage_vue_vue_type_style_index_0_id_cadc72c4_scoped_true_lang_css__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("5ac6"); +/* harmony import */ var _node_modules_vue_cli_service_node_modules_mini_css_extract_plugin_dist_loader_js_ref_7_oneOf_1_0_node_modules_vue_cli_service_node_modules_css_loader_dist_cjs_js_ref_7_oneOf_1_1_node_modules_vue_cli_service_node_modules_vue_loader_v16_dist_stylePostLoader_js_node_modules_postcss_loader_src_index_js_ref_7_oneOf_1_2_node_modules_vue_cli_service_node_modules_cache_loader_dist_cjs_js_ref_1_0_node_modules_vue_cli_service_node_modules_vue_loader_v16_dist_index_js_ref_1_1_SwaggerPage_vue_vue_type_style_index_0_id_cadc72c4_scoped_true_lang_css__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_node_modules_vue_cli_service_node_modules_mini_css_extract_plugin_dist_loader_js_ref_7_oneOf_1_0_node_modules_vue_cli_service_node_modules_css_loader_dist_cjs_js_ref_7_oneOf_1_1_node_modules_vue_cli_service_node_modules_vue_loader_v16_dist_stylePostLoader_js_node_modules_postcss_loader_src_index_js_ref_7_oneOf_1_2_node_modules_vue_cli_service_node_modules_cache_loader_dist_cjs_js_ref_1_0_node_modules_vue_cli_service_node_modules_vue_loader_v16_dist_index_js_ref_1_1_SwaggerPage_vue_vue_type_style_index_0_id_cadc72c4_scoped_true_lang_css__WEBPACK_IMPORTED_MODULE_0__); /* unused harmony reexport * */ /***/ }), -/***/ "19dc": -/***/ (function(module, exports) { - -module.exports = __WEBPACK_EXTERNAL_MODULE__19dc__; - -/***/ }), - -/***/ "848d": +/***/ "5157": /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; -/* harmony import */ var _node_modules_vue_cli_service_node_modules_mini_css_extract_plugin_dist_loader_js_ref_7_oneOf_1_0_node_modules_vue_cli_service_node_modules_css_loader_dist_cjs_js_ref_7_oneOf_1_1_node_modules_vue_cli_service_node_modules_vue_loader_v16_dist_stylePostLoader_js_node_modules_postcss_loader_src_index_js_ref_7_oneOf_1_2_node_modules_vue_cli_service_node_modules_cache_loader_dist_cjs_js_ref_1_0_node_modules_vue_cli_service_node_modules_vue_loader_v16_dist_index_js_ref_1_1_SwaggerUiPanel_vue_vue_type_style_index_0_id_7e9c9564_scoped_true_lang_css__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("003e"); -/* harmony import */ var _node_modules_vue_cli_service_node_modules_mini_css_extract_plugin_dist_loader_js_ref_7_oneOf_1_0_node_modules_vue_cli_service_node_modules_css_loader_dist_cjs_js_ref_7_oneOf_1_1_node_modules_vue_cli_service_node_modules_vue_loader_v16_dist_stylePostLoader_js_node_modules_postcss_loader_src_index_js_ref_7_oneOf_1_2_node_modules_vue_cli_service_node_modules_cache_loader_dist_cjs_js_ref_1_0_node_modules_vue_cli_service_node_modules_vue_loader_v16_dist_index_js_ref_1_1_SwaggerUiPanel_vue_vue_type_style_index_0_id_7e9c9564_scoped_true_lang_css__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_node_modules_vue_cli_service_node_modules_mini_css_extract_plugin_dist_loader_js_ref_7_oneOf_1_0_node_modules_vue_cli_service_node_modules_css_loader_dist_cjs_js_ref_7_oneOf_1_1_node_modules_vue_cli_service_node_modules_vue_loader_v16_dist_stylePostLoader_js_node_modules_postcss_loader_src_index_js_ref_7_oneOf_1_2_node_modules_vue_cli_service_node_modules_cache_loader_dist_cjs_js_ref_1_0_node_modules_vue_cli_service_node_modules_vue_loader_v16_dist_index_js_ref_1_1_SwaggerUiPanel_vue_vue_type_style_index_0_id_7e9c9564_scoped_true_lang_css__WEBPACK_IMPORTED_MODULE_0__); +/* harmony import */ var _node_modules_vue_cli_service_node_modules_mini_css_extract_plugin_dist_loader_js_ref_7_oneOf_1_0_node_modules_vue_cli_service_node_modules_css_loader_dist_cjs_js_ref_7_oneOf_1_1_node_modules_vue_cli_service_node_modules_vue_loader_v16_dist_stylePostLoader_js_node_modules_postcss_loader_src_index_js_ref_7_oneOf_1_2_node_modules_vue_cli_service_node_modules_cache_loader_dist_cjs_js_ref_1_0_node_modules_vue_cli_service_node_modules_vue_loader_v16_dist_index_js_ref_1_1_SwaggerUiPanel_vue_vue_type_style_index_0_id_49ed2978_scoped_true_lang_css__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("856e"); +/* harmony import */ var _node_modules_vue_cli_service_node_modules_mini_css_extract_plugin_dist_loader_js_ref_7_oneOf_1_0_node_modules_vue_cli_service_node_modules_css_loader_dist_cjs_js_ref_7_oneOf_1_1_node_modules_vue_cli_service_node_modules_vue_loader_v16_dist_stylePostLoader_js_node_modules_postcss_loader_src_index_js_ref_7_oneOf_1_2_node_modules_vue_cli_service_node_modules_cache_loader_dist_cjs_js_ref_1_0_node_modules_vue_cli_service_node_modules_vue_loader_v16_dist_index_js_ref_1_1_SwaggerUiPanel_vue_vue_type_style_index_0_id_49ed2978_scoped_true_lang_css__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_node_modules_vue_cli_service_node_modules_mini_css_extract_plugin_dist_loader_js_ref_7_oneOf_1_0_node_modules_vue_cli_service_node_modules_css_loader_dist_cjs_js_ref_7_oneOf_1_1_node_modules_vue_cli_service_node_modules_vue_loader_v16_dist_stylePostLoader_js_node_modules_postcss_loader_src_index_js_ref_7_oneOf_1_2_node_modules_vue_cli_service_node_modules_cache_loader_dist_cjs_js_ref_1_0_node_modules_vue_cli_service_node_modules_vue_loader_v16_dist_index_js_ref_1_1_SwaggerUiPanel_vue_vue_type_style_index_0_id_49ed2978_scoped_true_lang_css__WEBPACK_IMPORTED_MODULE_0__); /* unused harmony reexport * */ /***/ }), -/***/ "8bbf": -/***/ (function(module, exports) { +/***/ "5ac6": +/***/ (function(module, exports, __webpack_require__) { -module.exports = __WEBPACK_EXTERNAL_MODULE__8bbf__; +// extracted by mini-css-extract-plugin /***/ }), -/***/ "cd14": +/***/ "856e": /***/ (function(module, exports, __webpack_require__) { // extracted by mini-css-extract-plugin /***/ }), +/***/ "8bbf": +/***/ (function(module, exports) { + +module.exports = __WEBPACK_EXTERNAL_MODULE__8bbf__; + +/***/ }), + /***/ "fae3": /***/ (function(module, __webpack_exports__, __webpack_require__) { @@ -175,9 +175,9 @@ if (typeof window !== 'undefined') { // EXTERNAL MODULE: external {"commonjs":"vue","commonjs2":"vue","root":"Vue"} var external_commonjs_vue_commonjs2_vue_root_Vue_ = __webpack_require__("8bbf"); -// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-babel/node_modules/cache-loader/dist/cjs.js??ref--13-0!./node_modules/@vue/cli-plugin-babel/node_modules/thread-loader/dist/cjs.js!./node_modules/babel-loader/lib!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist/templateLoader.js??ref--6!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/OpenApiDocs/vue/src/SwaggerPage/SwaggerPage.vue?vue&type=template&id=7d1e8f2e&scoped=true +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-babel/node_modules/cache-loader/dist/cjs.js??ref--13-0!./node_modules/@vue/cli-plugin-babel/node_modules/thread-loader/dist/cjs.js!./node_modules/babel-loader/lib!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist/templateLoader.js??ref--6!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/ApiReference/vue/src/SwaggerPage/SwaggerPage.vue?vue&type=template&id=cadc72c4&scoped=true -const _withScopeId = n => (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["pushScopeId"])("data-v-7d1e8f2e"), n = n(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["popScopeId"])(), n); +const _withScopeId = n => (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["pushScopeId"])("data-v-cadc72c4"), n = n(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["popScopeId"])(), n); const _hoisted_1 = { class: "page" }; @@ -289,23 +289,23 @@ function render(_ctx, _cache, $props, $setup, $data, $options) { _: 1 })]); } -// CONCATENATED MODULE: ./plugins/OpenApiDocs/vue/src/SwaggerPage/SwaggerPage.vue?vue&type=template&id=7d1e8f2e&scoped=true +// CONCATENATED MODULE: ./plugins/ApiReference/vue/src/SwaggerPage/SwaggerPage.vue?vue&type=template&id=cadc72c4&scoped=true // EXTERNAL MODULE: external "CoreHome" var external_CoreHome_ = __webpack_require__("19dc"); -// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-babel/node_modules/cache-loader/dist/cjs.js??ref--13-0!./node_modules/@vue/cli-plugin-babel/node_modules/thread-loader/dist/cjs.js!./node_modules/babel-loader/lib!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist/templateLoader.js??ref--6!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/OpenApiDocs/vue/src/SwaggerPage/SwaggerUiPanel.vue?vue&type=template&id=7e9c9564&scoped=true +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-babel/node_modules/cache-loader/dist/cjs.js??ref--13-0!./node_modules/@vue/cli-plugin-babel/node_modules/thread-loader/dist/cjs.js!./node_modules/babel-loader/lib!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist/templateLoader.js??ref--6!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/ApiReference/vue/src/SwaggerPage/SwaggerUiPanel.vue?vue&type=template&id=49ed2978&scoped=true -const SwaggerUiPanelvue_type_template_id_7e9c9564_scoped_true_withScopeId = n => (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["pushScopeId"])("data-v-7e9c9564"), n = n(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["popScopeId"])(), n); -const SwaggerUiPanelvue_type_template_id_7e9c9564_scoped_true_hoisted_1 = { +const SwaggerUiPanelvue_type_template_id_49ed2978_scoped_true_withScopeId = n => (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["pushScopeId"])("data-v-49ed2978"), n = n(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["popScopeId"])(), n); +const SwaggerUiPanelvue_type_template_id_49ed2978_scoped_true_hoisted_1 = { key: 0, class: "swaggerLoader" }; -const SwaggerUiPanelvue_type_template_id_7e9c9564_scoped_true_hoisted_2 = ["id"]; -function SwaggerUiPanelvue_type_template_id_7e9c9564_scoped_true_render(_ctx, _cache, $props, $setup, $data, $options) { +const SwaggerUiPanelvue_type_template_id_49ed2978_scoped_true_hoisted_2 = ["id"]; +function SwaggerUiPanelvue_type_template_id_49ed2978_scoped_true_render(_ctx, _cache, $props, $setup, $data, $options) { const _component_ActivityIndicator = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("ActivityIndicator"); const _component_Alert = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("Alert"); - return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])(external_commonjs_vue_commonjs2_vue_root_Vue_["Fragment"], null, [_ctx.isLoading && !_ctx.spec && !_ctx.displayError ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", SwaggerUiPanelvue_type_template_id_7e9c9564_scoped_true_hoisted_1, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_ActivityIndicator, { + return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])(external_commonjs_vue_commonjs2_vue_root_Vue_["Fragment"], null, [_ctx.isLoading && !_ctx.spec && !_ctx.displayError ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", SwaggerUiPanelvue_type_template_id_49ed2978_scoped_true_hoisted_1, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_ActivityIndicator, { loading: true })])) : Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createCommentVNode"])("", true), _ctx.displayError ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createBlock"])(_component_Alert, { key: 1, @@ -318,11 +318,11 @@ function SwaggerUiPanelvue_type_template_id_7e9c9564_scoped_true_render(_ctx, _c class: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["normalizeClass"])(['swaggerMount', { 'swaggerMount--ready': _ctx.isReady }]) - }, null, 10, SwaggerUiPanelvue_type_template_id_7e9c9564_scoped_true_hoisted_2)], 64); + }, null, 10, SwaggerUiPanelvue_type_template_id_49ed2978_scoped_true_hoisted_2)], 64); } -// CONCATENATED MODULE: ./plugins/OpenApiDocs/vue/src/SwaggerPage/SwaggerUiPanel.vue?vue&type=template&id=7e9c9564&scoped=true +// CONCATENATED MODULE: ./plugins/ApiReference/vue/src/SwaggerPage/SwaggerUiPanel.vue?vue&type=template&id=49ed2978&scoped=true -// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-typescript/node_modules/cache-loader/dist/cjs.js??ref--15-0!./node_modules/babel-loader/lib!./node_modules/@vue/cli-plugin-typescript/node_modules/ts-loader??ref--15-2!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/OpenApiDocs/vue/src/SwaggerPage/SwaggerUiPanel.vue?vue&type=script&lang=ts +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-typescript/node_modules/cache-loader/dist/cjs.js??ref--15-0!./node_modules/babel-loader/lib!./node_modules/@vue/cli-plugin-typescript/node_modules/ts-loader??ref--15-2!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/ApiReference/vue/src/SwaggerPage/SwaggerUiPanel.vue?vue&type=script&lang=ts const activeCopySuccessStateKey = '__matomoActiveCopySuccessState'; @@ -405,6 +405,240 @@ const copySuccessIconMarkup = ' { + if (found || !pathItem || typeof pathItem !== 'object') { + return found; + } + const operation = pathItem[methodName]; + if (!operation || typeof operation !== 'object') { + return found; + } + return operation.operationId === operationId ? operation : found; + }, null); + }, + resolveComponentSpec(spec, componentType, refPrefix, value) { + if (!value || typeof value !== 'object') { + return null; + } + const ref = value.$ref; + if (typeof ref !== 'string' || !ref.startsWith(refPrefix)) { + return value; + } + const componentName = ref.substring(refPrefix.length); + const components = spec.components; + const componentGroup = components === null || components === void 0 ? void 0 : components[componentType]; + if (!componentGroup || typeof componentGroup !== 'object') { + return null; + } + const resolved = componentGroup[componentName]; + return resolved && typeof resolved === 'object' ? resolved : null; + }, + resolveParameterSpec(spec, parameter) { + return this.resolveComponentSpec(spec, 'parameters', '#/components/parameters/', parameter); + }, + resolveSchemaSpec(spec, schema) { + return this.resolveComponentSpec(spec, 'schemas', '#/components/schemas/', schema); + }, + isArrayQueryParameter(parameter) { + if (parameter.in !== 'query') { + return false; + } + const { + schema + } = parameter; + if (!schema || typeof schema !== 'object') { + return false; + } + const schemaObject = schema; + if (schemaObject.type === 'array') { + return true; + } + const { + oneOf + } = schemaObject; + if (!Array.isArray(oneOf)) { + return false; + } + return oneOf.some(branch => branch && typeof branch === 'object' && branch.type === 'array'); + }, + getArrayQueryParameterNamesForRequest(spec, operation) { + const parameters = operation === null || operation === void 0 ? void 0 : operation.parameters; + if (!Array.isArray(parameters)) { + return []; + } + return parameters.map(parameter => this.resolveParameterSpec(spec, parameter)).filter(parameter => Boolean(parameter)).filter(parameter => this.isArrayQueryParameter(parameter)).map(parameter => parameter.name).filter(name => typeof name === 'string' && name.length > 0); + }, + rewriteMatomoArrayQueryParams(request, spec, operation) { + if (!request.url) { + return request; + } + const arrayQueryParameterNames = this.getArrayQueryParameterNamesForRequest(spec, operation); + if (!arrayQueryParameterNames.length) { + return request; + } + const parsedUrl = new URL(request.url, window.location.href); + let didRewriteUrl = false; + arrayQueryParameterNames.forEach(parameterName => { + const values = parsedUrl.searchParams.getAll(parameterName); + if (!values.length) { + return; + } + parsedUrl.searchParams.delete(parameterName); + values.forEach((value, index) => { + parsedUrl.searchParams.append(`${parameterName}[${index}]`, value); + }); + didRewriteUrl = true; + }); + if (didRewriteUrl) { + request.url = parsedUrl.toString(); + } + return request; + }, + resolveFormRequestBodySchema(spec, operation) { + const requestBody = operation === null || operation === void 0 ? void 0 : operation.requestBody; + if (!requestBody || typeof requestBody !== 'object') { + return null; + } + const { + content + } = requestBody; + if (!content || typeof content !== 'object') { + return null; + } + const formContent = content['application/x-www-form-urlencoded']; + if (!formContent || typeof formContent !== 'object') { + return null; + } + return this.resolveSchemaSpec(spec, formContent.schema); + }, + isObjectLikeFormSchema(spec, schema) { + if (schema.type === 'object') { + return true; + } + if (schema.type !== 'array') { + return false; + } + const items = this.resolveSchemaSpec(spec, schema.items); + return Boolean((items === null || items === void 0 ? void 0 : items.type) === 'object'); + }, + getObjectFormSchemasForRequest(spec, operation) { + const schema = this.resolveFormRequestBodySchema(spec, operation); + const properties = schema === null || schema === void 0 ? void 0 : schema.properties; + if (!properties || typeof properties !== 'object') { + return {}; + } + return Object.entries(properties).reduce((result, [name, propertySchema]) => { + const resolved = this.resolveSchemaSpec(spec, propertySchema); + if (resolved && this.isObjectLikeFormSchema(spec, resolved)) { + result[name] = resolved; + } + return result; + }, {}); + }, + getRequestContentType(request) { + const { + headers + } = request; + if (!headers || typeof headers !== 'object') { + return ''; + } + const match = Object.entries(headers).find(([headerName]) => headerName.toLowerCase() === 'content-type'); + return typeof (match === null || match === void 0 ? void 0 : match[1]) === 'string' ? match[1] : ''; + }, + appendPhpFormEntries(target, keyPrefix, value) { + if (Array.isArray(value)) { + value.forEach((item, index) => { + this.appendPhpFormEntries(target, `${keyPrefix}[${index}]`, item); + }); + return; + } + if (value && typeof value === 'object') { + Object.entries(value).forEach(([key, nestedValue]) => { + this.appendPhpFormEntries(target, `${keyPrefix}[${key}]`, nestedValue); + }); + return; + } + target.append(keyPrefix, value == null ? '' : String(value)); + }, + parseObjectLikeFormValue(rawValue, schema) { + const tryParseJson = jsonValue => { + try { + const parsedValue = JSON.parse(jsonValue); + if (schema.type === 'array' && parsedValue && typeof parsedValue === 'object' && !Array.isArray(parsedValue)) { + return [parsedValue]; + } + return parsedValue; + } catch (error) { + return null; + } + }; + const parsedJson = tryParseJson(rawValue); + if (parsedJson !== null) { + return parsedJson; + } + const trimmedValue = rawValue.trim(); + if (!trimmedValue.startsWith('{') || !trimmedValue.endsWith('}')) { + return null; + } + const parsedJsonLikeObject = tryParseJson(schema.type === 'array' ? `[${trimmedValue.replace(/((?:"(?:\\.|[^"])*")|true|false|null|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|\}|\])(\s*")/g, '$1,$2')}]` : trimmedValue.replace(/((?:"(?:\\.|[^"])*")|true|false|null|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|\}|\])(\s*")/g, '$1,$2')); + if (parsedJsonLikeObject !== null) { + return parsedJsonLikeObject; + } + return schema.type === 'array' ? tryParseJson(`[${trimmedValue}]`) : null; + }, + rewriteMatomoObjectFormParams(request, spec, operation) { + if (!request.url) { + return request; + } + const contentType = this.getRequestContentType(request); + if (!contentType.includes('application/x-www-form-urlencoded')) { + return request; + } + const objectFormSchemas = this.getObjectFormSchemasForRequest(spec, operation); + const objectFormParameterNames = new Set(Object.keys(objectFormSchemas)); + if (!objectFormParameterNames.size) { + return request; + } + let rawBody = ''; + if (request.body instanceof URLSearchParams) { + rawBody = request.body.toString(); + } else if (typeof request.body === 'string') { + rawBody = request.body; + } + if (!rawBody) { + return request; + } + const parsedBody = new URLSearchParams(rawBody); + const rewrittenBody = new URLSearchParams(); + let didRewriteBody = false; + parsedBody.forEach((value, key) => { + if (!objectFormParameterNames.has(key)) { + rewrittenBody.append(key, value); + return; + } + const schema = objectFormSchemas[key]; + const parsedValue = this.parseObjectLikeFormValue(value, schema); + if (!parsedValue || typeof parsedValue !== 'object') { + rewrittenBody.append(key, value); + return; + } + this.appendPhpFormEntries(rewrittenBody, key, parsedValue); + didRewriteBody = true; + }); + if (didRewriteBody) { + request.body = rewrittenBody.toString(); + } + return request; + }, shortenSummaryPaths(swaggerRoot) { const summaryPaths = swaggerRoot.querySelectorAll('.opblock-summary-path'); summaryPaths.forEach(element => { @@ -540,15 +774,21 @@ const copySuccessIconMarkup = ' { + const operation = request.url ? this.getOperationForRequest(specWithCurrentInstanceUrl, request.url, request.method || 'get') : null; + const requestWithArrayQueryParams = this.rewriteMatomoArrayQueryParams(request, specWithCurrentInstanceUrl, operation); + return this.rewriteMatomoObjectFormParams(requestWithArrayQueryParams, specWithCurrentInstanceUrl, operation); + }, onComplete: () => { window.setTimeout(() => { this.normalizeSwaggerUi(container); @@ -560,22 +800,22 @@ const copySuccessIconMarkup = '(Object(i["pushScopeId"])("data-v-7d1e8f2e"),e=e(),Object(i["popScopeId"])(),e),s={class:"page"},c=["innerHTML"],l=["innerHTML"],u=["href"],p={key:1},d={key:2},g={class:"searchBar"},m=a(()=>Object(i["createElementVNode"])("span",{class:"searchIcon icon-search"},null,-1)),h=["placeholder"],y={key:0,class:"emptyText"},b={key:1,class:"pluginList"},f=["onMouseenter"],O=["aria-expanded","onFocus","onClick"],S={class:"pluginHeader"},j={class:"pluginName"},v={class:"card-content pluginBody"};function w(e,t,n,o,r,a){const w=Object(i["resolveComponent"])("ContentBlock"),P=Object(i["resolveComponent"])("ActivityIndicator"),k=Object(i["resolveComponent"])("Alert"),C=Object(i["resolveComponent"])("SwaggerUiPanel"),E=Object(i["resolveDirective"])("content-intro");return Object(i["openBlock"])(),Object(i["createElementBlock"])("div",s,[Object(i["withDirectives"])((Object(i["openBlock"])(),Object(i["createElementBlock"])("div",null,[Object(i["createElementVNode"])("h2",null,Object(i["toDisplayString"])(e.translate("OpenApiDocs_SwaggerApi")),1)])),[[E]]),Object(i["createVNode"])(w,{"content-title":e.translate("OpenApiDocs_ReportingApiReference")},{default:Object(i["withCtx"])(()=>[Object(i["createElementVNode"])("p",null,Object(i["toDisplayString"])(e.translate("OpenApiDocs_ReportingApiSummary")),1),Object(i["createElementVNode"])("p",{innerHTML:e.$sanitize(e.reportingApiMoreInformation)},null,8,c)]),_:1},8,["content-title"]),Object(i["createVNode"])(w,{"content-title":e.translate("OpenApiDocs_UserAuthentication")},{default:Object(i["withCtx"])(()=>[Object(i["createElementVNode"])("p",{innerHTML:e.$sanitize(e.userAuthenticationHelp)},null,8,l),Object(i["createElementVNode"])("p",null,[Object(i["createElementVNode"])("a",{href:e.userSecurityUrl},Object(i["toDisplayString"])(e.translate("OpenApiDocs_UserAuthenticationManageTokens")),9,u)])]),_:1},8,["content-title"]),Object(i["createVNode"])(w,null,{default:Object(i["withCtx"])(()=>[Object(i["createVNode"])(P,{loading:e.isLoading},null,8,["loading"]),e.loadError?(Object(i["openBlock"])(),Object(i["createBlock"])(k,{key:0,severity:"danger"},{default:Object(i["withCtx"])(()=>[Object(i["createTextVNode"])(Object(i["toDisplayString"])(e.loadError),1)]),_:1})):e.isLoading||0!==e.plugins.length?e.isLoading?Object(i["createCommentVNode"])("",!0):(Object(i["openBlock"])(),Object(i["createElementBlock"])("div",d,[Object(i["createElementVNode"])("div",g,[m,Object(i["withDirectives"])(Object(i["createElementVNode"])("input",{"onUpdate:modelValue":t[0]||(t[0]=t=>e.searchTerm=t),type:"text",class:"searchInput browser-default",placeholder:e.translate("OpenApiDocs_SwaggerPageSearchPlaceholder")},null,8,h),[[i["vModelText"],e.searchTerm]])]),0===e.filteredPlugins.length?(Object(i["openBlock"])(),Object(i["createElementBlock"])("p",y,Object(i["toDisplayString"])(e.translate("OpenApiDocs_SwaggerPageSearchNoResults")),1)):(Object(i["openBlock"])(),Object(i["createElementBlock"])("div",b,[(Object(i["openBlock"])(!0),Object(i["createElementBlock"])(i["Fragment"],null,Object(i["renderList"])(e.plugins,t=>Object(i["withDirectives"])((Object(i["openBlock"])(),Object(i["createElementBlock"])("div",{key:t,class:Object(i["normalizeClass"])(["card","pluginCard",{"pluginCard--expanded":e.expandedPluginName===t}]),onMouseenter:n=>e.prefetchPluginSpec(t)},[Object(i["createElementVNode"])("button",{type:"button",class:"pluginToggle","aria-expanded":e.expandedPluginName===t?"true":"false",onFocus:n=>e.prefetchPluginSpec(t),onClick:n=>e.togglePlugin(t)},[Object(i["createElementVNode"])("span",S,[Object(i["createElementVNode"])("span",{class:Object(i["normalizeClass"])(["pluginChevron",e.expandedPluginName===t?"icon-chevron-down":"icon-chevron-right"])},null,2),Object(i["createElementVNode"])("span",j,Object(i["toDisplayString"])(t),1)])],40,O),Object(i["createVNode"])(i["Transition"],{name:"pluginBodyTransition",onBeforeEnter:e.onPluginBodyBeforeEnter,onEnter:e.onPluginBodyEnter,onBeforeLeave:e.onPluginBodyBeforeLeave,onLeave:e.onPluginBodyLeave,onAfterEnter:e.resetPluginBodyTransitionStyles,onAfterLeave:e.resetPluginBodyTransitionStyles},{default:Object(i["withCtx"])(()=>[Object(i["withDirectives"])(Object(i["createElementVNode"])("div",v,[Object(i["createVNode"])(C,{plugin:t,"piwik-url":e.piwikUrl,spec:e.getPluginSpecState(t).spec,"is-loading":"loading"===e.getPluginSpecState(t).status,"spec-load-error":e.getPluginSpecState(t).loadError},null,8,["plugin","piwik-url","spec","is-loading","spec-load-error"])],512),[[i["vShow"],e.expandedPluginName===t]])]),_:2},1032,["onBeforeEnter","onEnter","onBeforeLeave","onLeave","onAfterEnter","onAfterLeave"])],42,f)),[[i["vShow"],e.filteredPluginSet.has(t)]])),128))]))])):(Object(i["openBlock"])(),Object(i["createElementBlock"])("p",p,Object(i["toDisplayString"])(e.translate("OpenApiDocs_SwaggerPagePluginEmpty")),1))]),_:1})])}var P=n("19dc");const k={key:0,class:"swaggerLoader"},C=["id"];function E(e,t,n,o,r,a){const s=Object(i["resolveComponent"])("ActivityIndicator"),c=Object(i["resolveComponent"])("Alert");return Object(i["openBlock"])(),Object(i["createElementBlock"])(i["Fragment"],null,[!e.isLoading||e.spec||e.displayError?Object(i["createCommentVNode"])("",!0):(Object(i["openBlock"])(),Object(i["createElementBlock"])("div",k,[Object(i["createVNode"])(s,{loading:!0})])),e.displayError?(Object(i["openBlock"])(),Object(i["createBlock"])(c,{key:1,severity:"danger"},{default:Object(i["withCtx"])(()=>[Object(i["createTextVNode"])(Object(i["toDisplayString"])(e.displayError),1)]),_:1})):Object(i["createCommentVNode"])("",!0),Object(i["createElementVNode"])("div",{id:e.swaggerContainerId,class:Object(i["normalizeClass"])(["swaggerMount",{"swaggerMount--ready":e.isReady}])},null,10,C)],64)}const B="__matomoActiveCopySuccessState",A="__matomoSummaryPathClickHandlerAttached",L="/index.php?module=API&method=",x=".opblock-tag, .opblock-summary, .expand-operation, .opblock-summary-control",T='',N='';var D=Object(i["defineComponent"])({components:{ActivityIndicator:P["ActivityIndicator"],Alert:P["Alert"]},props:{plugin:{type:String,required:!0},piwikUrl:{type:String,default:null},spec:{type:Object,default:null},isLoading:{type:Boolean,default:!1},specLoadError:{type:String,default:null}},data(){return{isReady:!1,loadError:null}},computed:{displayError(){return this.specLoadError||this.loadError},swaggerContainerId(){return"swagger-ui-"+this.plugin}},watch:{spec:{immediate:!0,handler(e){e?this.renderSwaggerUi():this.resetSwaggerUi()}}},beforeUnmount(){const e=this.getSwaggerRoot();e&&this.clearCopySuccessState(e)},methods:{getSwaggerRoot(){return document.getElementById(this.swaggerContainerId)},getSpecWithCurrentInstanceUrl(e){return this.piwikUrl?Object.assign(Object.assign({},e),{},{servers:[{url:this.piwikUrl}]}):e},shortenSummaryPaths(e){const t=e.querySelectorAll(".opblock-summary-path");t.forEach(e=>{const t=e.getAttribute("data-path");t&&t.startsWith(L)&&(e.textContent=t.substring(L.length),e.setAttribute("title",t))})},updateFlatSingleTag(e){const t=e.querySelectorAll(".opblock-tag-section");if(t.forEach(e=>{e.classList.remove("matomo-flat-tag")}),1!==t.length)return;const n=t[0];n.querySelector(":scope > .opblock-tag")&&n.classList.add("matomo-flat-tag")},applyMatomoCopyIcons(e){const t=e.querySelectorAll(".opblock-summary .view-line-link.copy-to-clipboard");t.forEach(e=>{e.classList.contains("matomo-copy-success")||(e.innerHTML=T)})},normalizeSwaggerUi(e){this.shortenSummaryPaths(e),this.updateFlatSingleTag(e),this.applyMatomoCopyIcons(e)},getSummaryPathCopyControl(e){return null===e||void 0===e?void 0:e.closest(".opblock-summary .view-line-link.copy-to-clipboard")},getFlatTagHeader(e){return null===e||void 0===e?void 0:e.closest(".opblock-tag-section.matomo-flat-tag > .opblock-tag")},clearCopySuccessState(e){const t=e[B];if(!t)return;const{element:n}=t;window.clearTimeout(t.resetTimeoutId),n.innerHTML=T,n.classList.remove("matomo-copy-success"),n.classList.remove("matomo-copy-reset"),window.requestAnimationFrame(()=>{n.classList.add("matomo-copy-reset"),window.setTimeout(()=>{n.classList.remove("matomo-copy-reset")},300)}),e[B]=null},showCopySuccessState(e,t){this.clearCopySuccessState(e),t.innerHTML=N,t.classList.remove("matomo-copy-reset"),t.classList.add("matomo-copy-success"),e[B]={element:t,resetTimeoutId:window.setTimeout(()=>{var n;(null===(n=e[B])||void 0===n?void 0:n.element)===t&&this.clearCopySuccessState(e)},3e3)}},attachSwaggerInteractionHandlers(e){e&&!e[A]&&(e[A]=!0,e.addEventListener("click",t=>{const n=t.target,o=this.getFlatTagHeader(n);if(o)return null!==n&&void 0!==n&&n.closest("a")||t.preventDefault(),void t.stopPropagation();const r=this.getSummaryPathCopyControl(n);r&&window.setTimeout(()=>{r.isConnected&&this.showCopySuccessState(e,r)},0),null!==n&&void 0!==n&&n.closest(x)&&window.setTimeout(()=>{this.normalizeSwaggerUi(e)},0)},!0))},resetSwaggerUi(){const e=this.getSwaggerRoot();this.isReady=!1,this.loadError=null,e&&(this.clearCopySuccessState(e),e.innerHTML="")},renderSwaggerUi(){var e;const t=window.SwaggerUIBundle,n=this.getSwaggerRoot();if(this.isReady=!1,this.loadError=null,!t||!n||!this.spec){if(!this.spec)return;return this.isReady=!0,void(this.loadError=Object(P["translate"])("OpenApiDocs_SwaggerPageSpecLoadFailed"))}n.innerHTML="",t({dom_id:"#"+this.swaggerContainerId,spec:this.getSpecWithCurrentInstanceUrl(this.spec),deepLinking:!1,docExpansion:"list",defaultModelsExpandDepth:-1,layout:"BaseLayout",tagsSorter:"alpha",presets:null!==(e=t.presets)&&void 0!==e&&e.apis?[t.presets.apis]:[],onComplete:()=>{window.setTimeout(()=>{this.normalizeSwaggerUi(n),this.isReady=!0},0),this.attachSwaggerInteractionHandlers(n)}})}}});n("848d");D.render=E,D.__scopeId="data-v-7e9c9564";var _=D,I=Object(i["defineComponent"])({props:{piwikUrl:{type:String,default:null}},components:{ActivityIndicator:P["ActivityIndicator"],Alert:P["Alert"],ContentBlock:P["ContentBlock"],SwaggerUiPanel:_},directives:{ContentIntro:P["ContentIntro"]},computed:{reportingApiMoreInformation(){return Object(P["translate"])("OpenApiDocs_ReportingApiMoreInformation",Object(P["externalLink"])("https://matomo.org/docs/analytics-api"),"",Object(P["externalLink"])("https://developer.matomo.org/api-reference/reporting-api"),"")},userAuthenticationHelp(){const e=Object(P["translate"])("OpenApiDocs_UserAuthenticationUsingTokenAuth","","","token_auth"),t=Object(P["translate"])("CoreHome_LearnMoreFullStop",Object(P["externalLink"])("https://developer.matomo.org/api-reference/reporting-api#authenticate-to-the-api-via-token_auth-parameter"),"");return`${e} ${t}`},userSecurityUrl(){return`?${P["MatomoUrl"].stringify(Object.assign(Object.assign({},P["MatomoUrl"].urlParsed.value),{},{module:"UsersManager",action:"userSecurity"}))}#/#authtokens`},filteredPlugins(){const e=this.searchTerm.trim().toLowerCase();return e?this.plugins.filter(t=>t.toLowerCase().includes(e)):this.plugins},filteredPluginSet(){return new Set(this.filteredPlugins)}},data(){return{expandedPluginName:null,isLoading:!1,loadError:null,plugins:[],pluginSpecs:{},searchTerm:""}},created(){this.fetchPlugins()},watch:{searchTerm(e){this.expandedPluginName&&!this.matchesSearch(this.expandedPluginName,e)&&(this.expandedPluginName=null)}},methods:{matchesSearch(e,t){const n=(null!==t&&void 0!==t?t:this.searchTerm).trim().toLowerCase();return e.toLowerCase().includes(n)},forceReflow(e){e.getBoundingClientRect()},getPluginBodyTransitionDuration(e){return Math.min(400,Math.max(180,Math.round(e/4)))},resetPluginBodyTransitionStyles(e){const t=e;t.style.height="",t.style.transitionDuration="",t.style.overflow=""},setPluginBodyTransitionState(e,t){e.style.height=t,e.style.overflow="hidden"},transitionPluginBody(e,t,n){this.setPluginBodyTransitionState(e,t),e.style.transitionDuration=this.getPluginBodyTransitionDuration(e.scrollHeight)+"ms",this.forceReflow(e),e.style.height=n},onPluginBodyBeforeEnter(e){this.setPluginBodyTransitionState(e,"0")},onPluginBodyEnter(e){const t=e;this.transitionPluginBody(t,"0",t.scrollHeight+"px")},onPluginBodyBeforeLeave(e){const t=e;this.setPluginBodyTransitionState(t,t.scrollHeight+"px")},onPluginBodyLeave(e){const t=e;this.transitionPluginBody(t,t.scrollHeight+"px","0")},async fetchPlugins(){this.expandedPluginName=null,this.isLoading=!0,this.loadError=null,this.plugins=[],this.pluginSpecs={},this.searchTerm="";try{const e=await P["AjaxHelper"].fetch({method:"OpenApiDocs.getAllowedPlugins"},{createErrorNotification:!1});this.plugins=[...e].sort((e,t)=>e.localeCompare(t))}catch(e){this.loadError=Object(P["translate"])("OpenApiDocs_SwaggerPageRequestFailed")}finally{this.isLoading=!1}},createPluginSpecState(){return{loadError:null,request:null,spec:null,status:"idle"}},getPluginSpecState(e){return this.pluginSpecs[e]||(this.pluginSpecs[e]=this.createPluginSpecState()),this.pluginSpecs[e]},async prefetchPluginSpec(e,t=!1){const n=this.getPluginSpecState(e);if(!t){if("loaded"===n.status)return n.spec;if(n.request)return n.request}return n.status="loading",n.loadError=null,n.request=(async()=>{try{const t=await P["AjaxHelper"].fetch({method:"OpenApiDocs.getOpenApiSpec",pluginName:e,format:"json"},{createErrorNotification:!1});return n.spec=t,n.status="loaded",t}catch(t){return n.spec=null,n.status="error",n.loadError=Object(P["translate"])("OpenApiDocs_SwaggerPageSpecLoadFailed"),null}finally{n.request=null}})(),n.request},togglePlugin(e){if(this.expandedPluginName===e)return void(this.expandedPluginName=null);this.expandedPluginName=e;const t=this.getPluginSpecState(e);"loaded"!==t.status&&this.prefetchPluginSpec(e,"error"===t.status)}}});n("0edb");I.render=w,I.__scopeId="data-v-7d1e8f2e";var M=I; +(function(e,t){"object"===typeof exports&&"object"===typeof module?module.exports=t(require("CoreHome"),require("vue")):"function"===typeof define&&define.amd?define(["CoreHome"],t):"object"===typeof exports?exports["OpenApiDocs"]=t(require("CoreHome"),require("vue")):e["OpenApiDocs"]=t(e["CoreHome"],e["Vue"])})("undefined"!==typeof self?self:this,(function(e,t){return function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}return r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"===typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e["default"]}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="plugins/ApiReference/vue/dist/",r(r.s="fae3")}({"19dc":function(t,r){t.exports=e},"4eeb":function(e,t,r){"use strict";r("5ac6")},5157:function(e,t,r){"use strict";r("856e")},"5ac6":function(e,t,r){},"856e":function(e,t,r){},"8bbf":function(e,r){e.exports=t},fae3:function(e,t,r){"use strict";if(r.r(t),r.d(t,"SwaggerPage",(function(){return R})),"undefined"!==typeof window){var n=window.document.currentScript,o=n&&n.src.match(/(.+\/)[^/]+\.js(\?.*)?$/);o&&(r.p=o[1])}var a=r("8bbf");const i=e=>(Object(a["pushScopeId"])("data-v-cadc72c4"),e=e(),Object(a["popScopeId"])(),e),s={class:"page"},c=["innerHTML"],l=["innerHTML"],u=["href"],p={key:1},d={key:2},g={class:"searchBar"},m=i(()=>Object(a["createElementVNode"])("span",{class:"searchIcon icon-search"},null,-1)),h=["placeholder"],y={key:0,class:"emptyText"},f={key:1,class:"pluginList"},b=["onMouseenter"],S=["aria-expanded","onFocus","onClick"],j={class:"pluginHeader"},O={class:"pluginName"},v={class:"card-content pluginBody"};function w(e,t,r,n,o,i){const w=Object(a["resolveComponent"])("ContentBlock"),P=Object(a["resolveComponent"])("ActivityIndicator"),E=Object(a["resolveComponent"])("Alert"),A=Object(a["resolveComponent"])("SwaggerUiPanel"),k=Object(a["resolveDirective"])("content-intro");return Object(a["openBlock"])(),Object(a["createElementBlock"])("div",s,[Object(a["withDirectives"])((Object(a["openBlock"])(),Object(a["createElementBlock"])("div",null,[Object(a["createElementVNode"])("h2",null,Object(a["toDisplayString"])(e.translate("OpenApiDocs_SwaggerApi")),1)])),[[k]]),Object(a["createVNode"])(w,{"content-title":e.translate("OpenApiDocs_ReportingApiReference")},{default:Object(a["withCtx"])(()=>[Object(a["createElementVNode"])("p",null,Object(a["toDisplayString"])(e.translate("OpenApiDocs_ReportingApiSummary")),1),Object(a["createElementVNode"])("p",{innerHTML:e.$sanitize(e.reportingApiMoreInformation)},null,8,c)]),_:1},8,["content-title"]),Object(a["createVNode"])(w,{"content-title":e.translate("OpenApiDocs_UserAuthentication")},{default:Object(a["withCtx"])(()=>[Object(a["createElementVNode"])("p",{innerHTML:e.$sanitize(e.userAuthenticationHelp)},null,8,l),Object(a["createElementVNode"])("p",null,[Object(a["createElementVNode"])("a",{href:e.userSecurityUrl},Object(a["toDisplayString"])(e.translate("OpenApiDocs_UserAuthenticationManageTokens")),9,u)])]),_:1},8,["content-title"]),Object(a["createVNode"])(w,null,{default:Object(a["withCtx"])(()=>[Object(a["createVNode"])(P,{loading:e.isLoading},null,8,["loading"]),e.loadError?(Object(a["openBlock"])(),Object(a["createBlock"])(E,{key:0,severity:"danger"},{default:Object(a["withCtx"])(()=>[Object(a["createTextVNode"])(Object(a["toDisplayString"])(e.loadError),1)]),_:1})):e.isLoading||0!==e.plugins.length?e.isLoading?Object(a["createCommentVNode"])("",!0):(Object(a["openBlock"])(),Object(a["createElementBlock"])("div",d,[Object(a["createElementVNode"])("div",g,[m,Object(a["withDirectives"])(Object(a["createElementVNode"])("input",{"onUpdate:modelValue":t[0]||(t[0]=t=>e.searchTerm=t),type:"text",class:"searchInput browser-default",placeholder:e.translate("OpenApiDocs_SwaggerPageSearchPlaceholder")},null,8,h),[[a["vModelText"],e.searchTerm]])]),0===e.filteredPlugins.length?(Object(a["openBlock"])(),Object(a["createElementBlock"])("p",y,Object(a["toDisplayString"])(e.translate("OpenApiDocs_SwaggerPageSearchNoResults")),1)):(Object(a["openBlock"])(),Object(a["createElementBlock"])("div",f,[(Object(a["openBlock"])(!0),Object(a["createElementBlock"])(a["Fragment"],null,Object(a["renderList"])(e.plugins,t=>Object(a["withDirectives"])((Object(a["openBlock"])(),Object(a["createElementBlock"])("div",{key:t,class:Object(a["normalizeClass"])(["card","pluginCard",{"pluginCard--expanded":e.expandedPluginName===t}]),onMouseenter:r=>e.prefetchPluginSpec(t)},[Object(a["createElementVNode"])("button",{type:"button",class:"pluginToggle","aria-expanded":e.expandedPluginName===t?"true":"false",onFocus:r=>e.prefetchPluginSpec(t),onClick:r=>e.togglePlugin(t)},[Object(a["createElementVNode"])("span",j,[Object(a["createElementVNode"])("span",{class:Object(a["normalizeClass"])(["pluginChevron",e.expandedPluginName===t?"icon-chevron-down":"icon-chevron-right"])},null,2),Object(a["createElementVNode"])("span",O,Object(a["toDisplayString"])(t),1)])],40,S),Object(a["createVNode"])(a["Transition"],{name:"pluginBodyTransition",onBeforeEnter:e.onPluginBodyBeforeEnter,onEnter:e.onPluginBodyEnter,onBeforeLeave:e.onPluginBodyBeforeLeave,onLeave:e.onPluginBodyLeave,onAfterEnter:e.resetPluginBodyTransitionStyles,onAfterLeave:e.resetPluginBodyTransitionStyles},{default:Object(a["withCtx"])(()=>[Object(a["withDirectives"])(Object(a["createElementVNode"])("div",v,[Object(a["createVNode"])(A,{plugin:t,"piwik-url":e.piwikUrl,spec:e.getPluginSpecState(t).spec,"is-loading":"loading"===e.getPluginSpecState(t).status,"spec-load-error":e.getPluginSpecState(t).loadError},null,8,["plugin","piwik-url","spec","is-loading","spec-load-error"])],512),[[a["vShow"],e.expandedPluginName===t]])]),_:2},1032,["onBeforeEnter","onEnter","onBeforeLeave","onLeave","onAfterEnter","onAfterLeave"])],42,b)),[[a["vShow"],e.filteredPluginSet.has(t)]])),128))]))])):(Object(a["openBlock"])(),Object(a["createElementBlock"])("p",p,Object(a["toDisplayString"])(e.translate("OpenApiDocs_SwaggerPagePluginEmpty")),1))]),_:1})])}var P=r("19dc");const E={key:0,class:"swaggerLoader"},A=["id"];function k(e,t,r,n,o,i){const s=Object(a["resolveComponent"])("ActivityIndicator"),c=Object(a["resolveComponent"])("Alert");return Object(a["openBlock"])(),Object(a["createElementBlock"])(a["Fragment"],null,[!e.isLoading||e.spec||e.displayError?Object(a["createCommentVNode"])("",!0):(Object(a["openBlock"])(),Object(a["createElementBlock"])("div",E,[Object(a["createVNode"])(s,{loading:!0})])),e.displayError?(Object(a["openBlock"])(),Object(a["createBlock"])(c,{key:1,severity:"danger"},{default:Object(a["withCtx"])(()=>[Object(a["createTextVNode"])(Object(a["toDisplayString"])(e.displayError),1)]),_:1})):Object(a["createCommentVNode"])("",!0),Object(a["createElementVNode"])("div",{id:e.swaggerContainerId,class:Object(a["normalizeClass"])(["swaggerMount",{"swaggerMount--ready":e.isReady}])},null,10,A)],64)}const C="__matomoActiveCopySuccessState",B="__matomoSummaryPathClickHandlerAttached",L="/index.php?module=API&method=",x=".opblock-tag, .opblock-summary, .expand-operation, .opblock-summary-control",T='',N='';var _=Object(a["defineComponent"])({components:{ActivityIndicator:P["ActivityIndicator"],Alert:P["Alert"]},props:{plugin:{type:String,required:!0},piwikUrl:{type:String,default:null},spec:{type:Object,default:null},isLoading:{type:Boolean,default:!1},specLoadError:{type:String,default:null}},data(){return{isReady:!1,loadError:null}},computed:{displayError(){return this.specLoadError||this.loadError},swaggerContainerId(){return"swagger-ui-"+this.plugin}},watch:{spec:{immediate:!0,handler(e){e?this.renderSwaggerUi():this.resetSwaggerUi()}}},beforeUnmount(){const e=this.getSwaggerRoot();e&&this.clearCopySuccessState(e)},methods:{getSwaggerRoot(){return document.getElementById(this.swaggerContainerId)},getSpecWithCurrentInstanceUrl(e){return this.piwikUrl?Object.assign(Object.assign({},e),{},{servers:[{url:this.piwikUrl}]}):e},getOperationForRequest(e,t,r){const n=new URL(t,window.location.href).searchParams.get("method"),{paths:o}=e;if(!n||!o||"object"!==typeof o)return null;const a=r.toLowerCase();return Object.values(o).reduce((e,t)=>{if(e||!t||"object"!==typeof t)return e;const r=t[a];return r&&"object"===typeof r&&r.operationId===n?r:e},null)},resolveComponentSpec(e,t,r,n){if(!n||"object"!==typeof n)return null;const o=n.$ref;if("string"!==typeof o||!o.startsWith(r))return n;const a=o.substring(r.length),i=e.components,s=null===i||void 0===i?void 0:i[t];if(!s||"object"!==typeof s)return null;const c=s[a];return c&&"object"===typeof c?c:null},resolveParameterSpec(e,t){return this.resolveComponentSpec(e,"parameters","#/components/parameters/",t)},resolveSchemaSpec(e,t){return this.resolveComponentSpec(e,"schemas","#/components/schemas/",t)},isArrayQueryParameter(e){if("query"!==e.in)return!1;const{schema:t}=e;if(!t||"object"!==typeof t)return!1;const r=t;if("array"===r.type)return!0;const{oneOf:n}=r;return!!Array.isArray(n)&&n.some(e=>e&&"object"===typeof e&&"array"===e.type)},getArrayQueryParameterNamesForRequest(e,t){const r=null===t||void 0===t?void 0:t.parameters;return Array.isArray(r)?r.map(t=>this.resolveParameterSpec(e,t)).filter(e=>Boolean(e)).filter(e=>this.isArrayQueryParameter(e)).map(e=>e.name).filter(e=>"string"===typeof e&&e.length>0):[]},rewriteMatomoArrayQueryParams(e,t,r){if(!e.url)return e;const n=this.getArrayQueryParameterNamesForRequest(t,r);if(!n.length)return e;const o=new URL(e.url,window.location.href);let a=!1;return n.forEach(e=>{const t=o.searchParams.getAll(e);t.length&&(o.searchParams.delete(e),t.forEach((t,r)=>{o.searchParams.append(`${e}[${r}]`,t)}),a=!0)}),a&&(e.url=o.toString()),e},resolveFormRequestBodySchema(e,t){const r=null===t||void 0===t?void 0:t.requestBody;if(!r||"object"!==typeof r)return null;const{content:n}=r;if(!n||"object"!==typeof n)return null;const o=n["application/x-www-form-urlencoded"];return o&&"object"===typeof o?this.resolveSchemaSpec(e,o.schema):null},isObjectLikeFormSchema(e,t){if("object"===t.type)return!0;if("array"!==t.type)return!1;const r=this.resolveSchemaSpec(e,t.items);return Boolean("object"===(null===r||void 0===r?void 0:r.type))},getObjectFormSchemasForRequest(e,t){const r=this.resolveFormRequestBodySchema(e,t),n=null===r||void 0===r?void 0:r.properties;return n&&"object"===typeof n?Object.entries(n).reduce((t,[r,n])=>{const o=this.resolveSchemaSpec(e,n);return o&&this.isObjectLikeFormSchema(e,o)&&(t[r]=o),t},{}):{}},getRequestContentType(e){const{headers:t}=e;if(!t||"object"!==typeof t)return"";const r=Object.entries(t).find(([e])=>"content-type"===e.toLowerCase());return"string"===typeof(null===r||void 0===r?void 0:r[1])?r[1]:""},appendPhpFormEntries(e,t,r){Array.isArray(r)?r.forEach((r,n)=>{this.appendPhpFormEntries(e,`${t}[${n}]`,r)}):r&&"object"===typeof r?Object.entries(r).forEach(([r,n])=>{this.appendPhpFormEntries(e,`${t}[${r}]`,n)}):e.append(t,null==r?"":String(r))},parseObjectLikeFormValue(e,t){const r=e=>{try{const r=JSON.parse(e);return"array"===t.type&&r&&"object"===typeof r&&!Array.isArray(r)?[r]:r}catch(r){return null}},n=r(e);if(null!==n)return n;const o=e.trim();if(!o.startsWith("{")||!o.endsWith("}"))return null;const a=r("array"===t.type?`[${o.replace(/((?:"(?:\\.|[^"])*")|true|false|null|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|\}|\])(\s*")/g,"$1,$2")}]`:o.replace(/((?:"(?:\\.|[^"])*")|true|false|null|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|\}|\])(\s*")/g,"$1,$2"));return null!==a?a:"array"===t.type?r(`[${o}]`):null},rewriteMatomoObjectFormParams(e,t,r){if(!e.url)return e;const n=this.getRequestContentType(e);if(!n.includes("application/x-www-form-urlencoded"))return e;const o=this.getObjectFormSchemasForRequest(t,r),a=new Set(Object.keys(o));if(!a.size)return e;let i="";if(e.body instanceof URLSearchParams?i=e.body.toString():"string"===typeof e.body&&(i=e.body),!i)return e;const s=new URLSearchParams(i),c=new URLSearchParams;let l=!1;return s.forEach((e,t)=>{if(!a.has(t))return void c.append(t,e);const r=o[t],n=this.parseObjectLikeFormValue(e,r);n&&"object"===typeof n?(this.appendPhpFormEntries(c,t,n),l=!0):c.append(t,e)}),l&&(e.body=c.toString()),e},shortenSummaryPaths(e){const t=e.querySelectorAll(".opblock-summary-path");t.forEach(e=>{const t=e.getAttribute("data-path");t&&t.startsWith(L)&&(e.textContent=t.substring(L.length),e.setAttribute("title",t))})},updateFlatSingleTag(e){const t=e.querySelectorAll(".opblock-tag-section");if(t.forEach(e=>{e.classList.remove("matomo-flat-tag")}),1!==t.length)return;const r=t[0];r.querySelector(":scope > .opblock-tag")&&r.classList.add("matomo-flat-tag")},applyMatomoCopyIcons(e){const t=e.querySelectorAll(".opblock-summary .view-line-link.copy-to-clipboard");t.forEach(e=>{e.classList.contains("matomo-copy-success")||(e.innerHTML=T)})},normalizeSwaggerUi(e){this.shortenSummaryPaths(e),this.updateFlatSingleTag(e),this.applyMatomoCopyIcons(e)},getSummaryPathCopyControl(e){return null===e||void 0===e?void 0:e.closest(".opblock-summary .view-line-link.copy-to-clipboard")},getFlatTagHeader(e){return null===e||void 0===e?void 0:e.closest(".opblock-tag-section.matomo-flat-tag > .opblock-tag")},clearCopySuccessState(e){const t=e[C];if(!t)return;const{element:r}=t;window.clearTimeout(t.resetTimeoutId),r.innerHTML=T,r.classList.remove("matomo-copy-success"),r.classList.remove("matomo-copy-reset"),window.requestAnimationFrame(()=>{r.classList.add("matomo-copy-reset"),window.setTimeout(()=>{r.classList.remove("matomo-copy-reset")},300)}),e[C]=null},showCopySuccessState(e,t){this.clearCopySuccessState(e),t.innerHTML=N,t.classList.remove("matomo-copy-reset"),t.classList.add("matomo-copy-success"),e[C]={element:t,resetTimeoutId:window.setTimeout(()=>{var r;(null===(r=e[C])||void 0===r?void 0:r.element)===t&&this.clearCopySuccessState(e)},3e3)}},attachSwaggerInteractionHandlers(e){e&&!e[B]&&(e[B]=!0,e.addEventListener("click",t=>{const r=t.target,n=this.getFlatTagHeader(r);if(n)return null!==r&&void 0!==r&&r.closest("a")||t.preventDefault(),void t.stopPropagation();const o=this.getSummaryPathCopyControl(r);o&&window.setTimeout(()=>{o.isConnected&&this.showCopySuccessState(e,o)},0),null!==r&&void 0!==r&&r.closest(x)&&window.setTimeout(()=>{this.normalizeSwaggerUi(e)},0)},!0))},resetSwaggerUi(){const e=this.getSwaggerRoot();this.isReady=!1,this.loadError=null,e&&(this.clearCopySuccessState(e),e.innerHTML="")},renderSwaggerUi(){var e;const t=window.SwaggerUIBundle,r=this.getSwaggerRoot();if(this.isReady=!1,this.loadError=null,!t||!r||!this.spec){if(!this.spec)return;return this.isReady=!0,void(this.loadError=Object(P["translate"])("OpenApiDocs_SwaggerPageSpecLoadFailed"))}r.innerHTML="";const n=this.getSpecWithCurrentInstanceUrl(this.spec);t({dom_id:"#"+this.swaggerContainerId,spec:n,deepLinking:!1,docExpansion:"list",defaultModelsExpandDepth:-1,layout:"BaseLayout",tagsSorter:"alpha",presets:null!==(e=t.presets)&&void 0!==e&&e.apis?[t.presets.apis]:[],requestInterceptor:e=>{const t=e.url?this.getOperationForRequest(n,e.url,e.method||"get"):null,r=this.rewriteMatomoArrayQueryParams(e,n,t);return this.rewriteMatomoObjectFormParams(r,n,t)},onComplete:()=>{window.setTimeout(()=>{this.normalizeSwaggerUi(r),this.isReady=!0},0),this.attachSwaggerInteractionHandlers(r)}})}}});r("5157");_.render=k,_.__scopeId="data-v-49ed2978";var D=_,M=Object(a["defineComponent"])({props:{piwikUrl:{type:String,default:null}},components:{ActivityIndicator:P["ActivityIndicator"],Alert:P["Alert"],ContentBlock:P["ContentBlock"],SwaggerUiPanel:D},directives:{ContentIntro:P["ContentIntro"]},computed:{reportingApiMoreInformation(){return Object(P["translate"])("OpenApiDocs_ReportingApiMoreInformation",Object(P["externalLink"])("https://matomo.org/docs/analytics-api"),"",Object(P["externalLink"])("https://developer.matomo.org/api-reference/reporting-api"),"")},userAuthenticationHelp(){const e=Object(P["translate"])("OpenApiDocs_UserAuthenticationUsingTokenAuth","","","token_auth"),t=Object(P["translate"])("CoreHome_LearnMoreFullStop",Object(P["externalLink"])("https://developer.matomo.org/api-reference/reporting-api#authenticate-to-the-api-via-token_auth-parameter"),"");return`${e} ${t}`},userSecurityUrl(){return`?${P["MatomoUrl"].stringify(Object.assign(Object.assign({},P["MatomoUrl"].urlParsed.value),{},{module:"UsersManager",action:"userSecurity"}))}#/#authtokens`},filteredPlugins(){const e=this.searchTerm.trim().toLowerCase();return e?this.plugins.filter(t=>t.toLowerCase().includes(e)):this.plugins},filteredPluginSet(){return new Set(this.filteredPlugins)}},data(){return{expandedPluginName:null,isLoading:!1,loadError:null,plugins:[],pluginSpecs:{},searchTerm:""}},created(){this.fetchPlugins()},watch:{searchTerm(e){this.expandedPluginName&&!this.matchesSearch(this.expandedPluginName,e)&&(this.expandedPluginName=null)}},methods:{matchesSearch(e,t){const r=(null!==t&&void 0!==t?t:this.searchTerm).trim().toLowerCase();return e.toLowerCase().includes(r)},forceReflow(e){e.getBoundingClientRect()},getPluginBodyTransitionDuration(e){return Math.min(400,Math.max(180,Math.round(e/4)))},resetPluginBodyTransitionStyles(e){const t=e;t.style.height="",t.style.transitionDuration="",t.style.overflow=""},setPluginBodyTransitionState(e,t){e.style.height=t,e.style.overflow="hidden"},transitionPluginBody(e,t,r){this.setPluginBodyTransitionState(e,t),e.style.transitionDuration=this.getPluginBodyTransitionDuration(e.scrollHeight)+"ms",this.forceReflow(e),e.style.height=r},onPluginBodyBeforeEnter(e){this.setPluginBodyTransitionState(e,"0")},onPluginBodyEnter(e){const t=e;this.transitionPluginBody(t,"0",t.scrollHeight+"px")},onPluginBodyBeforeLeave(e){const t=e;this.setPluginBodyTransitionState(t,t.scrollHeight+"px")},onPluginBodyLeave(e){const t=e;this.transitionPluginBody(t,t.scrollHeight+"px","0")},async fetchPlugins(){this.expandedPluginName=null,this.isLoading=!0,this.loadError=null,this.plugins=[],this.pluginSpecs={},this.searchTerm="";try{const e=await P["AjaxHelper"].fetch({method:"OpenApiDocs.getAllowedPlugins"},{createErrorNotification:!1});this.plugins=[...e].sort((e,t)=>e.localeCompare(t))}catch(e){this.loadError=Object(P["translate"])("OpenApiDocs_SwaggerPageRequestFailed")}finally{this.isLoading=!1}},createPluginSpecState(){return{loadError:null,request:null,spec:null,status:"idle"}},getPluginSpecState(e){return this.pluginSpecs[e]||(this.pluginSpecs[e]=this.createPluginSpecState()),this.pluginSpecs[e]},async prefetchPluginSpec(e,t=!1){const r=this.getPluginSpecState(e);if(!t){if("loaded"===r.status)return r.spec;if(r.request)return r.request}return r.status="loading",r.loadError=null,r.request=(async()=>{try{const t=await P["AjaxHelper"].fetch({method:"OpenApiDocs.getOpenApiSpec",pluginName:e,format:"json"},{createErrorNotification:!1});return r.spec=t,r.status="loaded",t}catch(t){return r.spec=null,r.status="error",r.loadError=Object(P["translate"])("OpenApiDocs_SwaggerPageSpecLoadFailed"),null}finally{r.request=null}})(),r.request},togglePlugin(e){if(this.expandedPluginName===e)return void(this.expandedPluginName=null);this.expandedPluginName=e;const t=this.getPluginSpecState(e);"loaded"!==t.status&&this.prefetchPluginSpec(e,"error"===t.status)}}});r("4eeb");M.render=w,M.__scopeId="data-v-cadc72c4";var R=M; /*! * Matomo - free/libre analytics platform * diff --git a/vue/src/SwaggerPage/SwaggerUiPanel.vue b/vue/src/SwaggerPage/SwaggerUiPanel.vue index c74edda..071650b 100644 --- a/vue/src/SwaggerPage/SwaggerUiPanel.vue +++ b/vue/src/SwaggerPage/SwaggerUiPanel.vue @@ -41,6 +41,14 @@ interface OpenApiSpec { [key: string]: unknown; } +interface SwaggerUiRequest { + method?: string; + url?: string; + body?: unknown; + headers?: Record; + [key: string]: unknown; +} + interface OpenApiServer { url?: string; [key: string]: unknown; @@ -55,6 +63,7 @@ type SwaggerUiFactory = (config: { onComplete?: () => void; plugins?: Array<() => unknown>; presets?: unknown[]; + requestInterceptor?: (request: SwaggerUiRequest) => SwaggerUiRequest; spec?: OpenApiSpec; tagsSorter?: string; }) => unknown; @@ -157,6 +166,336 @@ export default defineComponent({ servers: [{ url: this.piwikUrl } as OpenApiServer], }; }, + getOperationForRequest( + spec: OpenApiSpec, + requestUrl: string, + requestMethod: string, + ): Record | null { + const operationId = new URL(requestUrl, window.location.href).searchParams.get('method'); + const { paths } = spec; + + if (!operationId || !paths || typeof paths !== 'object') { + return null; + } + + const methodName = requestMethod.toLowerCase(); + return Object + .values(paths as Record) + .reduce | null>((found, pathItem) => { + if (found || !pathItem || typeof pathItem !== 'object') { + return found; + } + + const operation = (pathItem as Record)[methodName]; + if (!operation || typeof operation !== 'object') { + return found; + } + + return (operation as Record).operationId === operationId + ? operation as Record + : found; + }, null); + }, + resolveComponentSpec( + spec: OpenApiSpec, + componentType: 'parameters' | 'schemas', + refPrefix: string, + value: unknown, + ): Record | null { + if (!value || typeof value !== 'object') { + return null; + } + + const ref = (value as Record).$ref; + if (typeof ref !== 'string' || !ref.startsWith(refPrefix)) { + return value as Record; + } + + const componentName = ref.substring(refPrefix.length); + const components = spec.components as Record | undefined; + const componentGroup = components?.[componentType]; + if (!componentGroup || typeof componentGroup !== 'object') { + return null; + } + + const resolved = (componentGroup as Record)[componentName]; + return resolved && typeof resolved === 'object' ? resolved as Record : null; + }, + resolveParameterSpec(spec: OpenApiSpec, parameter: unknown): Record | null { + return this.resolveComponentSpec(spec, 'parameters', '#/components/parameters/', parameter); + }, + resolveSchemaSpec(spec: OpenApiSpec, schema: unknown): Record | null { + return this.resolveComponentSpec(spec, 'schemas', '#/components/schemas/', schema); + }, + isArrayQueryParameter(parameter: Record): boolean { + if (parameter.in !== 'query') { + return false; + } + + const { schema } = parameter; + if (!schema || typeof schema !== 'object') { + return false; + } + + const schemaObject = schema as Record; + if (schemaObject.type === 'array') { + return true; + } + + const { oneOf } = schemaObject; + if (!Array.isArray(oneOf)) { + return false; + } + + return oneOf.some( + (branch) => branch && typeof branch === 'object' && (branch as Record).type === 'array', + ); + }, + getArrayQueryParameterNamesForRequest( + spec: OpenApiSpec, + operation: Record | null, + ): string[] { + const parameters = operation?.parameters; + + if (!Array.isArray(parameters)) { + return []; + } + + return parameters + .map((parameter) => this.resolveParameterSpec(spec, parameter)) + .filter((parameter): parameter is Record => Boolean(parameter)) + .filter((parameter) => this.isArrayQueryParameter(parameter)) + .map((parameter) => parameter.name) + .filter((name): name is string => typeof name === 'string' && name.length > 0); + }, + rewriteMatomoArrayQueryParams( + request: SwaggerUiRequest, + spec: OpenApiSpec, + operation: Record | null, + ): SwaggerUiRequest { + if (!request.url) { + return request; + } + + const arrayQueryParameterNames = this.getArrayQueryParameterNamesForRequest(spec, operation); + if (!arrayQueryParameterNames.length) { + return request; + } + + const parsedUrl = new URL(request.url, window.location.href); + let didRewriteUrl = false; + + arrayQueryParameterNames.forEach((parameterName) => { + const values = parsedUrl.searchParams.getAll(parameterName); + if (!values.length) { + return; + } + + parsedUrl.searchParams.delete(parameterName); + values.forEach((value, index) => { + parsedUrl.searchParams.append(`${parameterName}[${index}]`, value); + }); + didRewriteUrl = true; + }); + + if (didRewriteUrl) { + request.url = parsedUrl.toString(); + } + + return request; + }, + resolveFormRequestBodySchema( + spec: OpenApiSpec, + operation: Record | null, + ): Record | null { + const requestBody = operation?.requestBody; + if (!requestBody || typeof requestBody !== 'object') { + return null; + } + + const { content } = requestBody as Record; + if (!content || typeof content !== 'object') { + return null; + } + + const formContent = (content as Record)['application/x-www-form-urlencoded']; + if (!formContent || typeof formContent !== 'object') { + return null; + } + + return this.resolveSchemaSpec(spec, (formContent as Record).schema); + }, + isObjectLikeFormSchema(spec: OpenApiSpec, schema: Record): boolean { + if (schema.type === 'object') { + return true; + } + + if (schema.type !== 'array') { + return false; + } + + const items = this.resolveSchemaSpec(spec, schema.items); + return Boolean(items?.type === 'object'); + }, + getObjectFormSchemasForRequest( + spec: OpenApiSpec, + operation: Record | null, + ): Record> { + const schema = this.resolveFormRequestBodySchema(spec, operation); + const properties = schema?.properties; + + if (!properties || typeof properties !== 'object') { + return {}; + } + + return Object.entries(properties as Record) + .reduce>>((result, [name, propertySchema]) => { + const resolved = this.resolveSchemaSpec(spec, propertySchema); + if (resolved && this.isObjectLikeFormSchema(spec, resolved)) { + result[name] = resolved; + } + + return result; + }, {}); + }, + getRequestContentType(request: SwaggerUiRequest): string { + const { headers } = request; + if (!headers || typeof headers !== 'object') { + return ''; + } + + const match = Object.entries(headers).find( + ([headerName]) => headerName.toLowerCase() === 'content-type', + ); + + return typeof match?.[1] === 'string' ? match[1] : ''; + }, + appendPhpFormEntries( + target: URLSearchParams, + keyPrefix: string, + value: unknown, + ) { + if (Array.isArray(value)) { + value.forEach((item, index) => { + this.appendPhpFormEntries(target, `${keyPrefix}[${index}]`, item); + }); + return; + } + + if (value && typeof value === 'object') { + Object.entries(value as Record).forEach(([key, nestedValue]) => { + this.appendPhpFormEntries(target, `${keyPrefix}[${key}]`, nestedValue); + }); + return; + } + + target.append(keyPrefix, value == null ? '' : String(value)); + }, + parseObjectLikeFormValue( + rawValue: string, + schema: Record, + ): unknown | null { + const tryParseJson = (jsonValue: string): unknown | null => { + try { + const parsedValue = JSON.parse(jsonValue) as unknown; + + if (schema.type === 'array' && parsedValue && typeof parsedValue === 'object' && !Array.isArray(parsedValue)) { + return [parsedValue]; + } + + return parsedValue; + } catch (error) { + return null; + } + }; + + const parsedJson = tryParseJson(rawValue); + if (parsedJson !== null) { + return parsedJson; + } + + const trimmedValue = rawValue.trim(); + if (!trimmedValue.startsWith('{') || !trimmedValue.endsWith('}')) { + return null; + } + + const parsedJsonLikeObject = tryParseJson( + schema.type === 'array' + ? `[${trimmedValue.replace( + /((?:"(?:\\.|[^"])*")|true|false|null|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|\}|\])(\s*")/g, + '$1,$2', + )}]` + : trimmedValue.replace( + /((?:"(?:\\.|[^"])*")|true|false|null|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|\}|\])(\s*")/g, + '$1,$2', + ), + ); + + if (parsedJsonLikeObject !== null) { + return parsedJsonLikeObject; + } + + return schema.type === 'array' ? tryParseJson(`[${trimmedValue}]`) : null; + }, + rewriteMatomoObjectFormParams( + request: SwaggerUiRequest, + spec: OpenApiSpec, + operation: Record | null, + ): SwaggerUiRequest { + if (!request.url) { + return request; + } + + const contentType = this.getRequestContentType(request); + if (!contentType.includes('application/x-www-form-urlencoded')) { + return request; + } + + const objectFormSchemas = this.getObjectFormSchemasForRequest(spec, operation); + const objectFormParameterNames = new Set(Object.keys(objectFormSchemas)); + + if (!objectFormParameterNames.size) { + return request; + } + + let rawBody = ''; + if (request.body instanceof URLSearchParams) { + rawBody = request.body.toString(); + } else if (typeof request.body === 'string') { + rawBody = request.body; + } + + if (!rawBody) { + return request; + } + + const parsedBody = new URLSearchParams(rawBody); + const rewrittenBody = new URLSearchParams(); + let didRewriteBody = false; + + parsedBody.forEach((value, key) => { + if (!objectFormParameterNames.has(key)) { + rewrittenBody.append(key, value); + return; + } + + const schema = objectFormSchemas[key]; + const parsedValue = this.parseObjectLikeFormValue(value, schema); + if (!parsedValue || typeof parsedValue !== 'object') { + rewrittenBody.append(key, value); + return; + } + + this.appendPhpFormEntries(rewrittenBody, key, parsedValue); + didRewriteBody = true; + }); + + if (didRewriteBody) { + request.body = rewrittenBody.toString(); + } + + return request; + }, shortenSummaryPaths(swaggerRoot: ParentNode) { const summaryPaths = swaggerRoot.querySelectorAll('.opblock-summary-path'); @@ -319,16 +658,32 @@ export default defineComponent({ } container.innerHTML = ''; + const specWithCurrentInstanceUrl = this.getSpecWithCurrentInstanceUrl(this.spec); swaggerUiBundle({ dom_id: `#${this.swaggerContainerId}`, - spec: this.getSpecWithCurrentInstanceUrl(this.spec), + spec: specWithCurrentInstanceUrl, deepLinking: false, docExpansion: 'list', defaultModelsExpandDepth: -1, layout: 'BaseLayout', tagsSorter: 'alpha', presets: swaggerUiBundle.presets?.apis ? [swaggerUiBundle.presets.apis] : [], + requestInterceptor: (request) => { + const operation = request.url + ? this.getOperationForRequest(specWithCurrentInstanceUrl, request.url, request.method || 'get') + : null; + const requestWithArrayQueryParams = this.rewriteMatomoArrayQueryParams( + request, + specWithCurrentInstanceUrl, + operation, + ); + return this.rewriteMatomoObjectFormParams( + requestWithArrayQueryParams, + specWithCurrentInstanceUrl, + operation, + ); + }, onComplete: () => { window.setTimeout(() => { this.normalizeSwaggerUi(container);