From 6fdd30d9f22fdd03d873c2d8e869b2d767bed824 Mon Sep 17 00:00:00 2001 From: Gokujo <19776684+Gokujo@users.noreply.github.com> Date: Sun, 1 Nov 2026 10:53:20 +0000 Subject: [PATCH] chore(deps): composer update --- upload/api/composer.lock | 407 ++++++++----- upload/api/vendor/composer/autoload_psr4.php | 1 + .../api/vendor/composer/autoload_static.php | 5 + upload/api/vendor/composer/installed.json | 240 ++++---- upload/api/vendor/composer/installed.php | 82 +-- .../vendor/lcobucci/jwt/src/Signer/Ecdsa.php | 13 +- .../vendor/lcobucci/jwt/src/Signer/Eddsa.php | 7 + .../vendor/lcobucci/jwt/src/Token/Parser.php | 8 +- .../Contracts/DomainHostInterface.php | 6 +- .../vendor/league/uri-interfaces/Encoder.php | 82 +-- .../league/uri-interfaces/HostRecord.php | 17 +- .../league/uri-interfaces/IPv4/Converter.php | 2 +- .../league/uri-interfaces/IPv6/Converter.php | 2 +- .../uri-interfaces/KeyValuePair/Converter.php | 2 +- .../league/uri-interfaces/Occurrence.php | 20 + .../league/uri-interfaces/QueryString.php | 4 +- .../league/uri-interfaces/UriString.php | 89 ++- .../api/vendor/league/uri/TypeConverter.php | 388 ++++++++++++ upload/api/vendor/league/uri/Uri.php | 30 +- upload/api/vendor/league/uri/UriTemplate.php | 79 ++- .../league/uri/UriTemplate/Expression.php | 292 +++++++++- .../league/uri/UriTemplate/ExtractedValue.php | 277 +++++++++ .../uri/UriTemplate/ExtractionErrorReason.php | 36 ++ .../uri/UriTemplate/ExtractionResult.php | 353 +++++++++++ .../vendor/league/uri/UriTemplate/Literal.php | 50 ++ .../league/uri/UriTemplate/Operator.php | 139 ++++- .../league/uri/UriTemplate/Template.php | 418 ++++++++++++- .../league/uri/UriTemplate/VarSpecifier.php | 15 +- .../league/uri/UriTemplate/VariableBag.php | 13 +- .../UriTemplate/VariableCanNotBeExtracted.php | 113 ++++ upload/api/vendor/league/uri/UrlPattern.php | 219 +++++++ .../league/uri/UrlPattern/Component.php | 60 ++ .../league/uri/UrlPattern/ComponentName.php | 26 + .../league/uri/UrlPattern/ComponentResult.php | 180 ++++++ .../league/uri/UrlPattern/MatchMode.php | 20 + .../vendor/league/uri/UrlPattern/Modifier.php | 22 + .../vendor/league/uri/UrlPattern/Parser.php | 446 ++++++++++++++ .../api/vendor/league/uri/UrlPattern/Part.php | 27 + .../vendor/league/uri/UrlPattern/PartType.php | 32 + .../league/uri/UrlPattern/PathToRegexp.php | 550 ++++++++++++++++++ .../league/uri/UrlPattern/PatternType.php | 20 + .../vendor/league/uri/UrlPattern/Result.php | 87 +++ .../vendor/league/uri/UrlPattern/State.php | 29 + .../vendor/league/uri/UrlPattern/Token.php | 24 + .../league/uri/UrlPattern/TokenType.php | 28 + .../vendor/league/uri/UrlPatternBuilder.php | 336 +++++++++++ upload/api/vendor/league/uri/Urn.php | 4 +- upload/api/vendor/league/uri/composer.json | 5 +- .../phpstan/phpdoc-parser/.agents/setup | 84 +++ .../vendor/phpstan/phpdoc-parser/README.md | 2 +- .../PhpDoc/TypeAliasImportTagValueNode.php | 10 +- .../src/Ast/Type/CallableTypeNode.php | 6 +- .../src/Ast/Type/FullyQualifiedNameNode.php | 34 ++ .../src/Ast/Type/GenericTypeNode.php | 6 +- .../phpdoc-parser/src/Ast/Type/NameNode.php | 37 ++ .../src/Ast/Type/NameNodeTrait.php | 61 ++ .../src/Ast/Type/RelativeNameNode.php | 37 ++ .../phpstan/phpdoc-parser/src/Lexer/Lexer.php | 2 +- .../phpdoc-parser/src/Parser/PhpDocParser.php | 21 +- .../src/Parser/TokenIterator.php | 33 ++ .../phpdoc-parser/src/Parser/TypeParser.php | 5 + .../phpdoc-parser/src/Printer/Printer.php | 222 ++++++- .../radebatz/type-info-extras/Readme.md | 5 +- .../src/TypeResolver/StringTypeResolver.php | 173 ++++-- .../vendor/symfony/console/Application.php | 12 +- .../console/Command/CompleteCommand.php | 50 ++ .../console/Command/InvokableCommand.php | 27 +- .../symfony/console/Command/LockableTrait.php | 16 +- .../AddConsoleCommandPass.php | 5 + .../console/Formatter/OutputFormatter.php | 271 ++++++++- .../Formatter/OutputFormatterStyle.php | 17 +- .../symfony/console/Helper/ProgressBar.php | 7 - .../vendor/symfony/console/Helper/Table.php | 238 ++++++-- .../console/Helper/TerminalInputHelper.php | 3 +- .../symfony/console/Input/ArgvInput.php | 24 +- .../symfony/console/Input/StringInput.php | 2 +- .../console/Output/ConsoleSectionOutput.php | 91 ++- .../api/vendor/symfony/finder/Gitignore.php | 28 +- upload/api/vendor/symfony/finder/Glob.php | 78 ++- .../Iterator/MultiplePcreFilterIterator.php | 4 +- .../Iterator/RecursiveDirectoryIterator.php | 11 + .../Iterator/VcsIgnoredFilterIterator.php | 69 ++- .../polyfill-intl-grapheme/Grapheme.php | 72 ++- .../symfony/polyfill-mbstring/Mbstring.php | 16 +- .../unidata/eastAsianWidthRegexp.php | 5 + .../Resources/unidata/titleCase.php | 139 +++++ .../vendor/symfony/string/AbstractString.php | 4 +- .../symfony/string/AbstractUnicodeString.php | 125 +++- .../vendor/symfony/string/CodePointString.php | 10 +- .../Resources/data/wcswidth_table_wide.php | 88 ++- .../Resources/data/wcswidth_table_zero.php | 56 +- .../symfony/string/Slugger/AsciiSlugger.php | 16 +- .../vendor/symfony/string/UnicodeString.php | 75 ++- .../TypeResolver/StringTypeResolver.php | 129 ++-- upload/api/vendor/symfony/yaml/Dumper.php | 83 +-- upload/api/vendor/symfony/yaml/Escaper.php | 2 +- .../symfony/yaml/Exception/ParseException.php | 4 +- upload/api/vendor/symfony/yaml/Inline.php | 54 +- upload/api/vendor/symfony/yaml/Parser.php | 52 +- .../api/vendor/zircote/swagger-php/README.md | 12 +- .../api/vendor/zircote/swagger-php/ROADMAP.md | 10 +- .../vendor/zircote/swagger-php/bin/openapi | 9 +- .../vendor/zircote/swagger-php/composer.json | 25 +- .../zircote/swagger-php/package-lock.json | 8 +- .../vendor/zircote/swagger-php/package.json | 2 +- .../zircote/swagger-php/phpstan.neon.dist | 36 -- .../api/vendor/zircote/swagger-php/rector.php | 1 - .../rector/set/classic-to-spec-classes.php | 57 ++ .../rector/set/classic-to-spec.php | 50 ++ .../rector/src/AliasSpecImportsRector.php | 243 ++++++++ .../rector/src/ComponentsHolderRector.php | 303 ++++++++++ .../src/DropParameterArgumentsRector.php | 105 ++++ .../rector/src/EncodedPropertyRector.php | 76 +++ .../rector/src/EncodingKeyRector.php | 83 +++ .../rector/src/MoveSchemaKeywordsRector.php | 190 ++++++ .../src/NamePositionalArgumentsRector.php | 215 +++++++ .../rector/src/ParameterStyleRector.php | 107 ++++ .../src/SecurityToRequirementRector.php | 171 ++++++ .../rector/src/TypeAliasesRector.php | 151 +++++ .../rector/src/TypeClassStringToRefRector.php | 98 ++++ .../rector/src/UnnestOpenApiRector.php | 185 ++++++ .../rector/src/WrapEnumClassStringRector.php | 87 +++ .../Analysers/AttributeAnnotationFactory.php | 10 + .../src/Analysers/ReflectionAnalyser.php | 14 +- .../src/Annotations/AbstractAnnotation.php | 39 +- .../src/Annotations/JsonSchemaTrait.php | 4 +- .../swagger-php/src/Annotations/OpenApi.php | 8 + .../swagger-php/src/Annotations/Operation.php | 3 +- .../zircote/swagger-php/src/Assembler.php | 2 +- .../Assembler/OperationMethodsTranslator.php | 175 ++++++ .../OptionalPropertyAttributeTranslator.php | 8 +- .../src/Attributes/AdditionalProperties.php | 2 +- .../swagger-php/src/Attributes/Items.php | 2 +- .../src/Attributes/JsonContent.php | 2 +- .../swagger-php/src/Attributes/Property.php | 2 +- .../swagger-php/src/Attributes/Schema.php | 2 +- .../swagger-php/src/Attributes/XmlContent.php | 2 +- .../swagger-php/src/Augmenter/Cleanup.php | 37 +- .../src/Augmenter/DefaultMediaType.php | 51 ++ .../swagger-php/src/Augmenter/Docblocks.php | 46 +- .../swagger-php/src/Augmenter/Enums.php | 7 +- .../src/Augmenter/ImpliedSchemas.php | 120 ++++ .../src/Augmenter/Inheritance/Operations.php | 11 +- .../src/Augmenter/Inheritance/Schemas.php | 4 +- .../swagger-php/src/Augmenter/Merge.php | 127 ++++ .../swagger-php/src/Augmenter/Names.php | 48 +- .../src/Augmenter/OperationIdTemplates.php | 47 ++ .../src/Augmenter/OperationIds.php | 13 +- .../src/Augmenter/OperationTags.php | 73 +++ .../swagger-php/src/Augmenter/Parameters.php | 65 +++ .../swagger-php/src/Augmenter/PathItems.php | 49 +- .../swagger-php/src/Augmenter/Properties.php | 172 ++++++ .../swagger-php/src/Augmenter/Refs.php | 77 ++- .../src/Augmenter/SecurityRequirements.php | 72 +++ .../swagger-php/src/Augmenter/Shortcuts.php | 71 ++- .../swagger-php/src/Augmenter/TypeAliases.php | 160 +++++ .../swagger-php/src/Augmenter/Types.php | 108 +++- .../zircote/swagger-php/src/Builder.php | 156 ++++- .../swagger-php/src/Builder/Result.php | 12 +- .../src/Compiler/OpenApi30Compiler.php | 165 +++++- .../src/Compiler/OpenApi31Compiler.php | 229 +++++++- .../src/Compiler/OpenApi32Compiler.php | 22 +- .../src/Console/GenerateCommand.php | 64 +- .../zircote/swagger-php/src/Context.php | 7 +- .../src/Contracts/AttributeInterface.php | 26 + .../src/Contracts/MergerInterface.php | 54 ++ .../zircote/swagger-php/src/HybridBridge.php | 147 ++++- .../src/Merge/Concerns/FoldTrait.php | 116 ++++ .../src/Merge/Concerns/IdentityTrait.php | 100 ++++ .../src/Merge/Concerns/ModeTrait.php | 45 ++ .../swagger-php/src/Merge/LastWins.php | 40 ++ .../zircote/swagger-php/src/Merge/Mode.php | 42 ++ .../swagger-php/src/Merge/Operations.php | 205 +++++++ .../zircote/swagger-php/src/Merge/Tags.php | 83 +++ .../zircote/swagger-php/src/Resolver.php | 28 +- .../src/Spec/AbstractAttribute.php | 66 +++ .../swagger-php/src/Spec/Components.php | 21 +- .../src/Spec/Concerns/SchemaInputTrait.php | 85 +++ .../zircote/swagger-php/src/Spec/Contact.php | 4 +- .../swagger-php/src/Spec/Discriminator.php | 4 +- .../zircote/swagger-php/src/Spec/Encoding.php | 4 +- .../zircote/swagger-php/src/Spec/Example.php | 18 +- .../src/Spec/ExternalDocumentation.php | 4 +- .../zircote/swagger-php/src/Spec/Fan.php | 21 + .../zircote/swagger-php/src/Spec/Flow.php | 6 +- .../src/Spec/Flow/AuthorizationCode.php | 23 + .../src/Spec/Flow/ClientCredentials.php | 21 + .../swagger-php/src/Spec/Flow/Implicit.php | 21 + .../swagger-php/src/Spec/Flow/Password.php | 21 + .../zircote/swagger-php/src/Spec/Format.php | 59 ++ .../zircote/swagger-php/src/Spec/Header.php | 18 +- .../swagger-php/src/Spec/HttpMethod.php | 1 + .../zircote/swagger-php/src/Spec/Info.php | 4 +- .../zircote/swagger-php/src/Spec/License.php | 4 +- .../zircote/swagger-php/src/Spec/Link.php | 15 +- .../swagger-php/src/Spec/MediaType.php | 16 +- .../swagger-php/src/Spec/MediaType/Json.php | 43 +- .../swagger-php/src/Spec/MediaType/Xml.php | 43 +- .../zircote/swagger-php/src/Spec/OpenApi.php | 4 +- .../swagger-php/src/Spec/Operation.php | 21 +- .../swagger-php/src/Spec/Operation/Delete.php | 14 + .../swagger-php/src/Spec/Operation/Get.php | 14 + .../swagger-php/src/Spec/Operation/Head.php | 14 + .../src/Spec/Operation/Options.php | 14 + .../swagger-php/src/Spec/Operation/Patch.php | 14 + .../swagger-php/src/Spec/Operation/Post.php | 14 + .../swagger-php/src/Spec/Operation/Put.php | 14 + .../swagger-php/src/Spec/Operation/Trace.php | 14 + .../swagger-php/src/Spec/Parameter.php | 23 +- .../swagger-php/src/Spec/Parameter/Cookie.php | 16 + .../swagger-php/src/Spec/Parameter/Header.php | 16 + .../swagger-php/src/Spec/Parameter/Path.php | 16 + .../swagger-php/src/Spec/Parameter/Query.php | 16 + .../zircote/swagger-php/src/Spec/PathItem.php | 17 +- .../zircote/swagger-php/src/Spec/Property.php | 4 +- .../swagger-php/src/Spec/Property/Encoded.php | 31 +- .../swagger-php/src/Spec/RequestBody.php | 35 +- .../zircote/swagger-php/src/Spec/Response.php | 33 +- .../zircote/swagger-php/src/Spec/Schema.php | 51 +- .../src/Spec/Schema/AdditionalProperties.php | 10 +- .../swagger-php/src/Spec/Schema/Items.php | 26 +- .../swagger-php/src/Spec/Schema/Ref.php | 10 +- .../src/Spec/Security/Requirement.php | 24 +- .../swagger-php/src/Spec/Security/Scheme.php | 22 +- .../src/Spec/Security/Scheme/ApiKey.php | 25 + .../src/Spec/Security/Scheme/Http.php | 25 + .../src/Spec/Security/Scheme/MutualTls.php | 21 + .../src/Spec/Security/Scheme/OAuth2.php | 23 + .../Spec/Security/Scheme/OpenIdConnect.php | 23 + .../zircote/swagger-php/src/Spec/Server.php | 4 +- .../swagger-php/src/Spec/ServerVariable.php | 4 +- .../zircote/swagger-php/src/Spec/Tag.php | 9 +- .../zircote/swagger-php/src/Spec/Type.php | 24 + .../zircote/swagger-php/src/Spec/Xml.php | 4 +- .../zircote/swagger-php/src/Specification.php | 4 + .../src/Specification/ComponentIndex.php | 23 + .../src/Specification/ComponentName.php | 120 +++- .../src/Specification/PathItemHierarchy.php | 78 +++ .../src/Type/LegacyTypeResolver.php | 13 +- .../src/Type/TypeInfoTypeResolver.php | 27 +- .../swagger-php/src/Type/TypeMapper.php | 27 +- .../swagger-php/src/Type/TypeResolver.php | 90 ++- .../src/Utils/AttributeFactory.php | 276 +++++++-- .../swagger-php/src/Utils/Pipeline.php | 18 +- .../src/Utils/SecurityRequirement.php | 36 ++ .../swagger-php/src/Utils/SourceLocation.php | 14 + .../swagger-php/src/Utils/TokenScanner.php | 5 +- 247 files changed, 13855 insertions(+), 1371 deletions(-) create mode 100644 upload/api/vendor/league/uri-interfaces/Occurrence.php create mode 100644 upload/api/vendor/league/uri/TypeConverter.php create mode 100644 upload/api/vendor/league/uri/UriTemplate/ExtractedValue.php create mode 100644 upload/api/vendor/league/uri/UriTemplate/ExtractionErrorReason.php create mode 100644 upload/api/vendor/league/uri/UriTemplate/ExtractionResult.php create mode 100644 upload/api/vendor/league/uri/UriTemplate/Literal.php create mode 100644 upload/api/vendor/league/uri/UriTemplate/VariableCanNotBeExtracted.php create mode 100644 upload/api/vendor/league/uri/UrlPattern.php create mode 100644 upload/api/vendor/league/uri/UrlPattern/Component.php create mode 100644 upload/api/vendor/league/uri/UrlPattern/ComponentName.php create mode 100644 upload/api/vendor/league/uri/UrlPattern/ComponentResult.php create mode 100644 upload/api/vendor/league/uri/UrlPattern/MatchMode.php create mode 100644 upload/api/vendor/league/uri/UrlPattern/Modifier.php create mode 100644 upload/api/vendor/league/uri/UrlPattern/Parser.php create mode 100644 upload/api/vendor/league/uri/UrlPattern/Part.php create mode 100644 upload/api/vendor/league/uri/UrlPattern/PartType.php create mode 100644 upload/api/vendor/league/uri/UrlPattern/PathToRegexp.php create mode 100644 upload/api/vendor/league/uri/UrlPattern/PatternType.php create mode 100644 upload/api/vendor/league/uri/UrlPattern/Result.php create mode 100644 upload/api/vendor/league/uri/UrlPattern/State.php create mode 100644 upload/api/vendor/league/uri/UrlPattern/Token.php create mode 100644 upload/api/vendor/league/uri/UrlPattern/TokenType.php create mode 100644 upload/api/vendor/league/uri/UrlPatternBuilder.php create mode 100755 upload/api/vendor/phpstan/phpdoc-parser/.agents/setup create mode 100644 upload/api/vendor/phpstan/phpdoc-parser/src/Ast/Type/FullyQualifiedNameNode.php create mode 100644 upload/api/vendor/phpstan/phpdoc-parser/src/Ast/Type/NameNode.php create mode 100644 upload/api/vendor/phpstan/phpdoc-parser/src/Ast/Type/NameNodeTrait.php create mode 100644 upload/api/vendor/phpstan/phpdoc-parser/src/Ast/Type/RelativeNameNode.php create mode 100644 upload/api/vendor/symfony/polyfill-mbstring/Resources/unidata/eastAsianWidthRegexp.php create mode 100644 upload/api/vendor/symfony/polyfill-mbstring/Resources/unidata/titleCase.php delete mode 100644 upload/api/vendor/zircote/swagger-php/phpstan.neon.dist create mode 100644 upload/api/vendor/zircote/swagger-php/rector/set/classic-to-spec-classes.php create mode 100644 upload/api/vendor/zircote/swagger-php/rector/set/classic-to-spec.php create mode 100644 upload/api/vendor/zircote/swagger-php/rector/src/AliasSpecImportsRector.php create mode 100644 upload/api/vendor/zircote/swagger-php/rector/src/ComponentsHolderRector.php create mode 100644 upload/api/vendor/zircote/swagger-php/rector/src/DropParameterArgumentsRector.php create mode 100644 upload/api/vendor/zircote/swagger-php/rector/src/EncodedPropertyRector.php create mode 100644 upload/api/vendor/zircote/swagger-php/rector/src/EncodingKeyRector.php create mode 100644 upload/api/vendor/zircote/swagger-php/rector/src/MoveSchemaKeywordsRector.php create mode 100644 upload/api/vendor/zircote/swagger-php/rector/src/NamePositionalArgumentsRector.php create mode 100644 upload/api/vendor/zircote/swagger-php/rector/src/ParameterStyleRector.php create mode 100644 upload/api/vendor/zircote/swagger-php/rector/src/SecurityToRequirementRector.php create mode 100644 upload/api/vendor/zircote/swagger-php/rector/src/TypeAliasesRector.php create mode 100644 upload/api/vendor/zircote/swagger-php/rector/src/TypeClassStringToRefRector.php create mode 100644 upload/api/vendor/zircote/swagger-php/rector/src/UnnestOpenApiRector.php create mode 100644 upload/api/vendor/zircote/swagger-php/rector/src/WrapEnumClassStringRector.php create mode 100644 upload/api/vendor/zircote/swagger-php/src/Assembler/OperationMethodsTranslator.php create mode 100644 upload/api/vendor/zircote/swagger-php/src/Augmenter/DefaultMediaType.php create mode 100644 upload/api/vendor/zircote/swagger-php/src/Augmenter/ImpliedSchemas.php create mode 100644 upload/api/vendor/zircote/swagger-php/src/Augmenter/Merge.php create mode 100644 upload/api/vendor/zircote/swagger-php/src/Augmenter/OperationIdTemplates.php create mode 100644 upload/api/vendor/zircote/swagger-php/src/Augmenter/OperationTags.php create mode 100644 upload/api/vendor/zircote/swagger-php/src/Augmenter/Parameters.php create mode 100644 upload/api/vendor/zircote/swagger-php/src/Augmenter/Properties.php create mode 100644 upload/api/vendor/zircote/swagger-php/src/Augmenter/SecurityRequirements.php create mode 100644 upload/api/vendor/zircote/swagger-php/src/Augmenter/TypeAliases.php create mode 100644 upload/api/vendor/zircote/swagger-php/src/Contracts/MergerInterface.php create mode 100644 upload/api/vendor/zircote/swagger-php/src/Merge/Concerns/FoldTrait.php create mode 100644 upload/api/vendor/zircote/swagger-php/src/Merge/Concerns/IdentityTrait.php create mode 100644 upload/api/vendor/zircote/swagger-php/src/Merge/Concerns/ModeTrait.php create mode 100644 upload/api/vendor/zircote/swagger-php/src/Merge/LastWins.php create mode 100644 upload/api/vendor/zircote/swagger-php/src/Merge/Mode.php create mode 100644 upload/api/vendor/zircote/swagger-php/src/Merge/Operations.php create mode 100644 upload/api/vendor/zircote/swagger-php/src/Merge/Tags.php create mode 100644 upload/api/vendor/zircote/swagger-php/src/Spec/Concerns/SchemaInputTrait.php create mode 100644 upload/api/vendor/zircote/swagger-php/src/Spec/Fan.php create mode 100644 upload/api/vendor/zircote/swagger-php/src/Spec/Format.php create mode 100644 upload/api/vendor/zircote/swagger-php/src/Spec/Type.php create mode 100644 upload/api/vendor/zircote/swagger-php/src/Utils/SecurityRequirement.php diff --git a/upload/api/composer.lock b/upload/api/composer.lock index 4cd50529..b3642aa2 100644 --- a/upload/api/composer.lock +++ b/upload/api/composer.lock @@ -71,7 +71,8 @@ "issues": "https://github.com/defuse/php-encryption/issues", "source": "https://github.com/defuse/php-encryption/tree/v2.4.0" }, - "time": "2023-06-19T06:10:36+00:00" + "time": "2023-06-19T06:10:36+00:00", + "published-time": "2023-06-19T06:16:55+00:00" }, { "name": "fig/http-message-util", @@ -127,7 +128,8 @@ "issues": "https://github.com/php-fig/http-message-util/issues", "source": "https://github.com/php-fig/http-message-util/tree/1.1.5" }, - "time": "2020-11-24T22:02:12+00:00" + "time": "2020-11-24T22:02:12+00:00", + "published-time": "2020-11-24T22:02:59+00:00" }, { "name": "laravel/serializable-closure", @@ -188,20 +190,21 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2026-09-22T14:32:34+00:00" + "time": "2026-09-22T14:32:34+00:00", + "published-time": "2026-09-22T14:33:13+00:00" }, { "name": "lcobucci/jwt", - "version": "5.6.0", + "version": "5.6.1", "source": { "type": "git", "url": "https://github.com/lcobucci/jwt.git", - "reference": "bb3e9f21e4196e8afc41def81ef649c164bca25e" + "reference": "482760068e5a55a9045feddb3e2fc3f878cbd577" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/lcobucci/jwt/zipball/bb3e9f21e4196e8afc41def81ef649c164bca25e", - "reference": "bb3e9f21e4196e8afc41def81ef649c164bca25e", + "url": "https://api.github.com/repos/lcobucci/jwt/zipball/482760068e5a55a9045feddb3e2fc3f878cbd577", + "reference": "482760068e5a55a9045feddb3e2fc3f878cbd577", "shasum": "" }, "require": { @@ -249,7 +252,7 @@ ], "support": { "issues": "https://github.com/lcobucci/jwt/issues", - "source": "https://github.com/lcobucci/jwt/tree/5.6.0" + "source": "https://github.com/lcobucci/jwt/tree/5.6.1" }, "funding": [ { @@ -261,7 +264,8 @@ "type": "patreon" } ], - "time": "2025-10-17T11:30:53+00:00" + "time": "2026-10-10T09:42:14+00:00", + "published-time": "2026-10-10T09:43:25+00:00" }, { "name": "league/event", @@ -320,7 +324,8 @@ "issues": "https://github.com/thephpleague/event/issues", "source": "https://github.com/thephpleague/event/tree/3.0.3" }, - "time": "2024-09-04T16:06:53+00:00" + "time": "2024-09-04T16:06:53+00:00", + "published-time": "2024-09-04T20:43:40+00:00" }, { "name": "league/oauth2-server", @@ -417,24 +422,26 @@ "type": "github" } ], - "time": "2026-06-25T15:24:07+00:00" + "time": "2026-06-25T15:24:07+00:00", + "published-time": "2026-06-25T15:28:46+00:00" }, { "name": "league/uri", - "version": "7.8.1", + "version": "7.9.1", "source": { "type": "git", "url": "https://github.com/thephpleague/uri.git", - "reference": "08cf38e3924d4f56238125547b5720496fac8fd4" + "reference": "40403f5c3141457a61f7f1aba6706e8fa4a0ece5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri/zipball/08cf38e3924d4f56238125547b5720496fac8fd4", - "reference": "08cf38e3924d4f56238125547b5720496fac8fd4", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/40403f5c3141457a61f7f1aba6706e8fa4a0ece5", + "reference": "40403f5c3141457a61f7f1aba6706e8fa4a0ece5", "shasum": "" }, "require": { - "league/uri-interfaces": "^7.8.1", + "ext-mbstring": "*", + "league/uri-interfaces": "^7.9.1", "php": "^8.1", "psr/http-factory": "^1" }, @@ -507,7 +514,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri/tree/7.8.1" + "source": "https://github.com/thephpleague/uri/tree/7.9.1" }, "funding": [ { @@ -515,20 +522,21 @@ "type": "github" } ], - "time": "2026-03-15T20:22:25+00:00" + "time": "2026-10-27T07:14:23+00:00", + "published-time": "2026-10-28T08:38:15+00:00" }, { "name": "league/uri-interfaces", - "version": "7.8.1", + "version": "7.9.1", "source": { "type": "git", "url": "https://github.com/thephpleague/uri-interfaces.git", - "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928" + "reference": "994300a79e17f179112dc259f8df0c4de982f736" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/85d5c77c5d6d3af6c54db4a78246364908f3c928", - "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/994300a79e17f179112dc259f8df0c4de982f736", + "reference": "994300a79e17f179112dc259f8df0c4de982f736", "shasum": "" }, "require": { @@ -591,7 +599,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.1" + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.9.1" }, "funding": [ { @@ -599,7 +607,8 @@ "type": "github" } ], - "time": "2026-03-08T20:05:35+00:00" + "time": "2026-10-27T07:14:23+00:00", + "published-time": "2026-10-28T08:38:21+00:00" }, { "name": "monolog/monolog", @@ -704,7 +713,8 @@ "type": "tidelift" } ], - "time": "2026-09-29T19:21:12+00:00" + "time": "2026-09-29T19:21:12+00:00", + "published-time": "2026-09-29T19:21:44+00:00" }, { "name": "nikic/fast-route", @@ -754,7 +764,8 @@ "issues": "https://github.com/nikic/FastRoute/issues", "source": "https://github.com/nikic/FastRoute/tree/1.3.1" }, - "time": "2026-07-09T19:38:47+00:00" + "time": "2026-07-09T19:38:47+00:00", + "published-time": "2026-07-09T19:39:54+00:00" }, { "name": "nikic/php-parser", @@ -811,7 +822,8 @@ "issues": "https://github.com/nikic/PHP-Parser/issues", "source": "https://github.com/nikic/PHP-Parser/tree/v5.9.0" }, - "time": "2026-09-13T18:51:52+00:00" + "time": "2026-09-13T18:51:52+00:00", + "published-time": "2026-09-13T18:53:16+00:00" }, { "name": "paragonie/random_compat", @@ -861,7 +873,8 @@ "issues": "https://github.com/paragonie/random_compat/issues", "source": "https://github.com/paragonie/random_compat" }, - "time": "2020-10-15T08:29:30+00:00" + "time": "2020-10-15T08:29:30+00:00", + "published-time": "2020-10-15T08:34:16+00:00" }, { "name": "php-di/invoker", @@ -916,7 +929,8 @@ "type": "github" } ], - "time": "2025-08-30T10:22:22+00:00" + "time": "2025-08-30T10:22:22+00:00", + "published-time": "2025-08-30T10:22:43+00:00" }, { "name": "php-di/php-di", @@ -989,20 +1003,21 @@ "type": "tidelift" } ], - "time": "2025-08-16T11:10:48+00:00" + "time": "2025-08-16T11:10:48+00:00", + "published-time": "2025-08-16T11:11:08+00:00" }, { "name": "phpstan/phpdoc-parser", - "version": "2.3.9", + "version": "2.4.1", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "2c122d2f890ae4b9e6ede898a9ac36b2c07e2a17" + "reference": "af8e840551031d254cfdb3a18a62c3470a309c10" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/2c122d2f890ae4b9e6ede898a9ac36b2c07e2a17", - "reference": "2c122d2f890ae4b9e6ede898a9ac36b2c07e2a17", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/af8e840551031d254cfdb3a18a62c3470a309c10", + "reference": "af8e840551031d254cfdb3a18a62c3470a309c10", "shasum": "" }, "require": { @@ -1034,9 +1049,10 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.9" + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.4.1" }, - "time": "2026-10-27T22:32:31+00:00" + "time": "2026-10-30T19:36:28+00:00", + "published-time": "2026-10-30T19:37:11+00:00" }, { "name": "psr/clock", @@ -1084,7 +1100,8 @@ "issues": "https://github.com/php-fig/clock/issues", "source": "https://github.com/php-fig/clock/tree/1.0.0" }, - "time": "2022-11-25T14:36:26+00:00" + "time": "2022-11-25T14:36:26+00:00", + "published-time": "2022-11-25T14:59:22+00:00" }, { "name": "psr/container", @@ -1137,7 +1154,8 @@ "issues": "https://github.com/php-fig/container/issues", "source": "https://github.com/php-fig/container/tree/2.0.2" }, - "time": "2021-11-05T16:47:00+00:00" + "time": "2021-11-05T16:47:00+00:00", + "published-time": "2021-11-05T16:53:27+00:00" }, { "name": "psr/event-dispatcher", @@ -1187,7 +1205,8 @@ "issues": "https://github.com/php-fig/event-dispatcher/issues", "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" }, - "time": "2019-01-08T18:20:26+00:00" + "time": "2019-01-08T18:20:26+00:00", + "published-time": "2019-03-25T15:31:44+00:00" }, { "name": "psr/http-factory", @@ -1242,7 +1261,8 @@ "support": { "source": "https://github.com/php-fig/http-factory" }, - "time": "2024-04-15T12:06:14+00:00" + "time": "2024-04-15T12:06:14+00:00", + "published-time": "2024-05-10T07:32:19+00:00" }, { "name": "psr/http-message", @@ -1295,7 +1315,8 @@ "support": { "source": "https://github.com/php-fig/http-message/tree/2.0" }, - "time": "2023-04-04T09:54:51+00:00" + "time": "2023-04-04T09:54:51+00:00", + "published-time": "2023-04-04T09:55:35+00:00" }, { "name": "psr/http-server-handler", @@ -1351,7 +1372,8 @@ "support": { "source": "https://github.com/php-fig/http-server-handler/tree/1.0.2" }, - "time": "2023-04-10T20:06:20+00:00" + "time": "2023-04-10T20:06:20+00:00", + "published-time": "2023-04-10T20:07:30+00:00" }, { "name": "psr/http-server-middleware", @@ -1408,7 +1430,8 @@ "issues": "https://github.com/php-fig/http-server-middleware/issues", "source": "https://github.com/php-fig/http-server-middleware/tree/1.0.2" }, - "time": "2023-04-11T06:14:47+00:00" + "time": "2023-04-11T06:14:47+00:00", + "published-time": "2023-04-11T06:15:22+00:00" }, { "name": "psr/log", @@ -1458,7 +1481,8 @@ "support": { "source": "https://github.com/php-fig/log/tree/3.0.2" }, - "time": "2024-09-11T13:17:53+00:00" + "time": "2024-09-11T13:17:53+00:00", + "published-time": "2024-09-11T13:21:14+00:00" }, { "name": "radebatz/type-info-extras", @@ -1520,7 +1544,8 @@ "issues": "https://github.com/DerManoMann/type-info-extras/issues", "source": "https://github.com/DerManoMann/type-info-extras/tree/1.0.11" }, - "time": "2026-10-26T22:24:45+00:00" + "time": "2026-10-26T22:24:45+00:00", + "published-time": "2026-10-26T22:25:56+00:00" }, { "name": "ralouphie/getallheaders", @@ -1564,7 +1589,8 @@ "issues": "https://github.com/ralouphie/getallheaders/issues", "source": "https://github.com/ralouphie/getallheaders/tree/develop" }, - "time": "2019-03-08T08:55:37+00:00" + "time": "2019-03-08T08:55:37+00:00", + "published-time": "2019-03-08T09:01:32+00:00" }, { "name": "slim/psr7", @@ -1643,7 +1669,8 @@ "issues": "https://github.com/slimphp/Slim-Psr7/issues", "source": "https://github.com/slimphp/Slim-Psr7/tree/1.8.0" }, - "time": "2025-11-02T17:51:19+00:00" + "time": "2025-11-02T17:51:19+00:00", + "published-time": "2025-11-05T19:46:07+00:00" }, { "name": "slim/slim", @@ -1759,20 +1786,21 @@ "type": "tidelift" } ], - "time": "2026-09-01T17:11:05+00:00" + "time": "2026-09-01T17:11:05+00:00", + "published-time": "2026-09-01T17:13:05+00:00" }, { "name": "symfony/console", - "version": "v7.4.21", + "version": "v7.4.22", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "21f1bc2296f7ff6043241a8dcbc09559b3732820" + "reference": "8012f842110b414fd1cc641adb0ed8423353b0bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/21f1bc2296f7ff6043241a8dcbc09559b3732820", - "reference": "21f1bc2296f7ff6043241a8dcbc09559b3732820", + "url": "https://api.github.com/repos/symfony/console/zipball/8012f842110b414fd1cc641adb0ed8423353b0bb", + "reference": "8012f842110b414fd1cc641adb0ed8423353b0bb", "shasum": "" }, "require": { @@ -1837,7 +1865,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.4.21" + "source": "https://github.com/symfony/console/tree/v7.4.22" }, "funding": [ { @@ -1857,7 +1885,8 @@ "type": "tidelift" } ], - "time": "2026-10-12T18:45:38+00:00" + "time": "2026-10-28T11:08:27+00:00", + "published-time": "2026-10-28T18:21:33+00:00" }, { "name": "symfony/deprecation-contracts", @@ -1928,20 +1957,21 @@ "type": "tidelift" } ], - "time": "2026-06-05T06:23:12+00:00" + "time": "2026-06-05T06:23:12+00:00", + "published-time": "2026-06-27T21:46:34+00:00" }, { "name": "symfony/finder", - "version": "v7.4.20", + "version": "v7.4.22", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "2dd31e5208db7a0fda7bfc998ad727b79c2e4d1e" + "reference": "e13420a8e0a76bdde1e7be09f923e62ad13739f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/2dd31e5208db7a0fda7bfc998ad727b79c2e4d1e", - "reference": "2dd31e5208db7a0fda7bfc998ad727b79c2e4d1e", + "url": "https://api.github.com/repos/symfony/finder/zipball/e13420a8e0a76bdde1e7be09f923e62ad13739f2", + "reference": "e13420a8e0a76bdde1e7be09f923e62ad13739f2", "shasum": "" }, "require": { @@ -1976,7 +2006,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.4.20" + "source": "https://github.com/symfony/finder/tree/v7.4.22" }, "funding": [ { @@ -1996,7 +2026,8 @@ "type": "tidelift" } ], - "time": "2026-09-26T06:31:10+00:00" + "time": "2026-10-19T16:56:54+00:00", + "published-time": "2026-10-28T18:21:14+00:00" }, { "name": "symfony/polyfill-ctype", @@ -2079,7 +2110,8 @@ "type": "tidelift" } ], - "time": "2026-04-10T16:19:22+00:00" + "time": "2026-04-10T16:19:22+00:00", + "published-time": "2026-04-26T14:44:50+00:00" }, { "name": "symfony/polyfill-intl-grapheme", @@ -2161,7 +2193,8 @@ "type": "tidelift" } ], - "time": "2026-10-15T09:18:53+00:00" + "time": "2026-10-15T09:18:53+00:00", + "published-time": "2026-10-28T10:07:44+00:00" }, { "name": "symfony/polyfill-intl-normalizer", @@ -2246,7 +2279,8 @@ "type": "tidelift" } ], - "time": "2026-09-23T22:02:09+00:00" + "time": "2026-09-23T22:02:09+00:00", + "published-time": "2026-09-30T10:23:47+00:00" }, { "name": "symfony/polyfill-mbstring", @@ -2331,7 +2365,8 @@ "type": "tidelift" } ], - "time": "2026-10-28T09:35:49+00:00" + "time": "2026-10-28T09:35:49+00:00", + "published-time": "2026-10-28T10:07:45+00:00" }, { "name": "symfony/service-contracts", @@ -2418,20 +2453,21 @@ "type": "tidelift" } ], - "time": "2026-07-27T15:39:01+00:00" + "time": "2026-07-27T15:39:01+00:00", + "published-time": "2026-08-30T19:56:37+00:00" }, { "name": "symfony/string", - "version": "v7.4.21", + "version": "v7.4.22", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "9bc35d074ad16a58c21cd52b18c5cacc4f4c3981" + "reference": "8dff7da3a56638f1d189c5368183294102de4ced" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/9bc35d074ad16a58c21cd52b18c5cacc4f4c3981", - "reference": "9bc35d074ad16a58c21cd52b18c5cacc4f4c3981", + "url": "https://api.github.com/repos/symfony/string/zipball/8dff7da3a56638f1d189c5368183294102de4ced", + "reference": "8dff7da3a56638f1d189c5368183294102de4ced", "shasum": "" }, "require": { @@ -2489,7 +2525,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.4.21" + "source": "https://github.com/symfony/string/tree/v7.4.22" }, "funding": [ { @@ -2509,20 +2545,21 @@ "type": "tidelift" } ], - "time": "2026-10-03T15:15:30+00:00" + "time": "2026-10-23T16:51:52+00:00", + "published-time": "2026-10-28T18:21:18+00:00" }, { "name": "symfony/type-info", - "version": "v7.4.20", + "version": "v7.4.22", "source": { "type": "git", "url": "https://github.com/symfony/type-info.git", - "reference": "bbaaf3366400f5c85f66800b955c77b46e8abad2" + "reference": "f69ab198af73e1aac485c0ff5aa066d20c14adf0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/type-info/zipball/bbaaf3366400f5c85f66800b955c77b46e8abad2", - "reference": "bbaaf3366400f5c85f66800b955c77b46e8abad2", + "url": "https://api.github.com/repos/symfony/type-info/zipball/f69ab198af73e1aac485c0ff5aa066d20c14adf0", + "reference": "f69ab198af73e1aac485c0ff5aa066d20c14adf0", "shasum": "" }, "require": { @@ -2572,7 +2609,7 @@ "type" ], "support": { - "source": "https://github.com/symfony/type-info/tree/v7.4.20" + "source": "https://github.com/symfony/type-info/tree/v7.4.22" }, "funding": [ { @@ -2592,20 +2629,21 @@ "type": "tidelift" } ], - "time": "2026-09-28T12:19:37+00:00" + "time": "2026-10-24T08:22:23+00:00", + "published-time": "2026-10-28T18:20:01+00:00" }, { "name": "symfony/yaml", - "version": "v7.4.21", + "version": "v7.4.22", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "044907929c7999814378be22da34722c7a86bad4" + "reference": "1ed6afa13d48c579c51cbf32e38f50686313497b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/044907929c7999814378be22da34722c7a86bad4", - "reference": "044907929c7999814378be22da34722c7a86bad4", + "url": "https://api.github.com/repos/symfony/yaml/zipball/1ed6afa13d48c579c51cbf32e38f50686313497b", + "reference": "1ed6afa13d48c579c51cbf32e38f50686313497b", "shasum": "" }, "require": { @@ -2648,7 +2686,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.4.21" + "source": "https://github.com/symfony/yaml/tree/v7.4.22" }, "funding": [ { @@ -2668,20 +2706,21 @@ "type": "tidelift" } ], - "time": "2026-10-12T12:34:30+00:00" + "time": "2026-10-18T14:39:27+00:00", + "published-time": "2026-10-28T18:20:33+00:00" }, { "name": "zircote/swagger-php", - "version": "6.18.0", + "version": "6.20.1", "source": { "type": "git", "url": "https://github.com/zircote/swagger-php.git", - "reference": "c93b7430193dc461602a2a3f4f6a0791a3a0608a" + "reference": "34f1bcc414012cf0ffbc747b0dc418bb67090c2f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zircote/swagger-php/zipball/c93b7430193dc461602a2a3f4f6a0791a3a0608a", - "reference": "c93b7430193dc461602a2a3f4f6a0791a3a0608a", + "url": "https://api.github.com/repos/zircote/swagger-php/zipball/34f1bcc414012cf0ffbc747b0dc418bb67090c2f", + "reference": "34f1bcc414012cf0ffbc747b0dc418bb67090c2f", "shasum": "" }, "require": { @@ -2690,13 +2729,16 @@ "phpstan/phpdoc-parser": "^2.0", "psr/log": "^1.1 || ^2.0 || ^3.0", "radebatz/type-info-extras": "^1.0.2", - "symfony/console": "^6.4 || ^7.0 || ^8.0", + "symfony/console": "^5.4 || ^6.4 || ^7.0 || ^8.0", "symfony/deprecation-contracts": "^2 || ^3", "symfony/finder": "^5.0 || ^6.0 || ^7.0 || ^8.0", "symfony/yaml": "^5.4 || ^6.0 || ^7.0 || ^8.0" }, "conflict": { - "symfony/process": ">=6, <6.4.14" + "symfony/console": "<5.4.35", + "symfony/finder": "<5.3.7", + "symfony/process": ">=6, <6.4.14", + "symfony/string": "<5.4.41" }, "require-dev": { "doctrine/annotations": "^2.0", @@ -2750,7 +2792,7 @@ ], "support": { "issues": "https://github.com/zircote/swagger-php/issues", - "source": "https://github.com/zircote/swagger-php/tree/6.18.0" + "source": "https://github.com/zircote/swagger-php/tree/6.20.1" }, "funding": [ { @@ -2758,22 +2800,23 @@ "type": "github" } ], - "time": "2026-10-20T22:51:12+00:00" + "time": "2026-10-30T21:52:05+00:00", + "published-time": "2026-10-30T21:54:53+00:00" } ], "packages-dev": [ { "name": "guzzlehttp/guzzle", - "version": "8.3.0", + "version": "8.3.1", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "b920e5fcaf81d761b8fabe600ca90999beaefdb7" + "reference": "7d9b1f430bb65ac664e120af227bf9a795ad6333" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b920e5fcaf81d761b8fabe600ca90999beaefdb7", - "reference": "b920e5fcaf81d761b8fabe600ca90999beaefdb7", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7d9b1f430bb65ac664e120af227bf9a795ad6333", + "reference": "7d9b1f430bb65ac664e120af227bf9a795ad6333", "shasum": "" }, "require": { @@ -2870,7 +2913,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/8.3.0" + "source": "https://github.com/guzzle/guzzle/tree/8.3.1" }, "funding": [ { @@ -2886,7 +2929,8 @@ "type": "tidelift" } ], - "time": "2026-10-18T13:38:43+00:00" + "time": "2026-10-25T21:35:34+00:00", + "published-time": "2026-10-25T21:37:39+00:00" }, { "name": "guzzlehttp/promises", @@ -2970,7 +3014,8 @@ "type": "tidelift" } ], - "time": "2026-10-18T12:48:16+00:00" + "time": "2026-10-18T12:48:16+00:00", + "published-time": "2026-10-18T12:48:47+00:00" }, { "name": "guzzlehttp/psr7", @@ -3089,7 +3134,8 @@ "type": "tidelift" } ], - "time": "2026-10-17T23:57:19+00:00" + "time": "2026-10-17T23:57:19+00:00", + "published-time": "2026-10-17T23:57:53+00:00" }, { "name": "myclabs/deep-copy", @@ -3149,7 +3195,8 @@ "type": "github" } ], - "time": "2026-08-11T10:17:44+00:00" + "time": "2026-08-11T10:17:44+00:00", + "published-time": "2026-08-11T10:29:20+00:00" }, { "name": "phar-io/manifest", @@ -3216,7 +3263,8 @@ "type": "github" } ], - "time": "2024-03-03T12:33:53+00:00" + "time": "2024-03-03T12:33:53+00:00", + "published-time": "2024-03-03T12:39:44+00:00" }, { "name": "phar-io/version", @@ -3267,29 +3315,30 @@ "issues": "https://github.com/phar-io/version/issues", "source": "https://github.com/phar-io/version/tree/3.2.1" }, - "time": "2022-02-21T01:04:05+00:00" + "time": "2022-02-21T01:04:05+00:00", + "published-time": "2022-02-21T01:04:30+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "12.5.7", + "version": "12.5.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "186dab580576598076de6818596d12b61801880e" + "reference": "7440368d94738d54f2c1de24dbd3871793bbb0dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/186dab580576598076de6818596d12b61801880e", - "reference": "186dab580576598076de6818596d12b61801880e", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7440368d94738d54f2c1de24dbd3871793bbb0dd", + "reference": "7440368d94738d54f2c1de24dbd3871793bbb0dd", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^5.7.0", + "nikic/php-parser": "^5.9.0", "php": ">=8.3", - "phpunit/php-text-template": "^5.0", + "phpunit/php-text-template": "^5.0.1", "sebastian/complexity": "^5.0", "sebastian/environment": "^8.1.2", "sebastian/lines-of-code": "^4.0.1", @@ -3297,7 +3346,7 @@ "theseer/tokenizer": "^2.0.1" }, "require-dev": { - "phpunit/phpunit": "^12.5.28" + "phpunit/phpunit": "^12.5.37" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -3335,7 +3384,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.7" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.8" }, "funding": [ { @@ -3355,7 +3404,8 @@ "type": "tidelift" } ], - "time": "2026-06-01T13:24:19+00:00" + "time": "2026-10-03T14:03:50+00:00", + "published-time": "2026-10-03T14:04:33+00:00" }, { "name": "phpunit/php-file-iterator", @@ -3428,7 +3478,8 @@ "type": "tidelift" } ], - "time": "2026-08-25T14:40:53+00:00" + "time": "2026-08-25T14:40:53+00:00", + "published-time": "2026-08-25T14:41:57+00:00" }, { "name": "phpunit/php-invoker", @@ -3492,27 +3543,28 @@ "type": "github" } ], - "time": "2025-02-07T04:58:58+00:00" + "time": "2025-02-07T04:58:58+00:00", + "published-time": "2025-02-07T04:59:09+00:00" }, { "name": "phpunit/php-text-template", - "version": "5.0.0", + "version": "5.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53" + "reference": "babab73b7ef9fae406f2eb3ee11323a61caa63d5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/e1367a453f0eda562eedb4f659e13aa900d66c53", - "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/babab73b7ef9fae406f2eb3ee11323a61caa63d5", + "reference": "babab73b7ef9fae406f2eb3ee11323a61caa63d5", "shasum": "" }, "require": { "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^12.5.37" }, "type": "library", "extra": { @@ -3544,15 +3596,28 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-text-template/issues", "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/5.0.0" + "source": "https://github.com/sebastianbergmann/php-text-template/tree/5.0.1" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-text-template", + "type": "tidelift" } ], - "time": "2025-02-07T04:59:16+00:00" + "time": "2026-10-03T04:51:29+00:00", + "published-time": "2026-10-03T04:54:39+00:00" }, { "name": "phpunit/php-timer", @@ -3612,20 +3677,21 @@ "type": "github" } ], - "time": "2025-02-07T04:59:38+00:00" + "time": "2025-02-07T04:59:38+00:00", + "published-time": "2025-02-07T04:59:52+00:00" }, { "name": "phpunit/phpunit", - "version": "12.5.37", + "version": "12.5.42", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "684340c1590e5f3f3b859f5e80619725d88b63ba" + "reference": "1d6dbe748c079e5d2b03bca1be69d0f4cc52a578" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/684340c1590e5f3f3b859f5e80619725d88b63ba", - "reference": "684340c1590e5f3f3b859f5e80619725d88b63ba", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/1d6dbe748c079e5d2b03bca1be69d0f4cc52a578", + "reference": "1d6dbe748c079e5d2b03bca1be69d0f4cc52a578", "shasum": "" }, "require": { @@ -3639,10 +3705,10 @@ "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.3", - "phpunit/php-code-coverage": "^12.5.7", + "phpunit/php-code-coverage": "^12.5.8", "phpunit/php-file-iterator": "^6.0.2", "phpunit/php-invoker": "^6.0.0", - "phpunit/php-text-template": "^5.0.0", + "phpunit/php-text-template": "^5.0.1", "phpunit/php-timer": "^8.0.0", "sebastian/cli-parser": "^4.2.1", "sebastian/comparator": "^7.1.8", @@ -3653,7 +3719,7 @@ "sebastian/object-enumerator": "^7.0.0", "sebastian/recursion-context": "^7.0.1", "sebastian/type": "^6.0.4", - "sebastian/version": "^6.0.0", + "sebastian/version": "^6.0.1", "staabm/side-effects-detector": "^1.0.5" }, "bin": [ @@ -3694,7 +3760,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.37" + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.42" }, "funding": [ { @@ -3702,7 +3768,8 @@ "type": "other" } ], - "time": "2026-09-29T12:08:54+00:00" + "time": "2026-10-27T04:15:03+00:00", + "published-time": "2026-10-27T04:18:06+00:00" }, { "name": "psr/http-client", @@ -3754,7 +3821,8 @@ "support": { "source": "https://github.com/php-fig/http-client" }, - "time": "2023-09-23T14:17:50+00:00" + "time": "2023-09-23T14:17:50+00:00", + "published-time": "2023-09-23T14:18:12+00:00" }, { "name": "sebastian/cli-parser", @@ -3823,7 +3891,8 @@ "type": "tidelift" } ], - "time": "2026-05-17T05:29:34+00:00" + "time": "2026-05-17T05:29:34+00:00", + "published-time": "2026-05-17T05:30:34+00:00" }, { "name": "sebastian/comparator", @@ -3915,7 +3984,8 @@ "type": "tidelift" } ], - "time": "2026-05-21T04:45:25+00:00" + "time": "2026-05-21T04:45:25+00:00", + "published-time": "2026-05-21T04:45:36+00:00" }, { "name": "sebastian/complexity", @@ -3973,7 +4043,8 @@ "type": "github" } ], - "time": "2025-02-07T04:55:25+00:00" + "time": "2025-02-07T04:55:25+00:00", + "published-time": "2025-02-07T04:55:36+00:00" }, { "name": "sebastian/diff", @@ -4052,7 +4123,8 @@ "type": "tidelift" } ], - "time": "2026-08-25T15:35:54+00:00" + "time": "2026-08-25T15:35:54+00:00", + "published-time": "2026-08-25T15:37:16+00:00" }, { "name": "sebastian/environment", @@ -4128,7 +4200,8 @@ "type": "tidelift" } ], - "time": "2026-05-25T13:40:20+00:00" + "time": "2026-05-25T13:40:20+00:00", + "published-time": "2026-05-25T13:40:39+00:00" }, { "name": "sebastian/exporter", @@ -4218,7 +4291,8 @@ "type": "tidelift" } ], - "time": "2026-05-20T04:37:17+00:00" + "time": "2026-05-20T04:37:17+00:00", + "published-time": "2026-05-20T04:37:31+00:00" }, { "name": "sebastian/global-state", @@ -4292,7 +4366,8 @@ "type": "tidelift" } ], - "time": "2026-06-01T15:10:33+00:00" + "time": "2026-06-01T15:10:33+00:00", + "published-time": "2026-06-01T15:20:45+00:00" }, { "name": "sebastian/lines-of-code", @@ -4362,7 +4437,8 @@ "type": "tidelift" } ], - "time": "2026-05-19T16:22:07+00:00" + "time": "2026-05-19T16:22:07+00:00", + "published-time": "2026-05-19T16:22:47+00:00" }, { "name": "sebastian/object-enumerator", @@ -4420,7 +4496,8 @@ "type": "github" } ], - "time": "2025-02-07T04:57:48+00:00" + "time": "2025-02-07T04:57:48+00:00", + "published-time": "2025-02-07T04:58:08+00:00" }, { "name": "sebastian/object-reflector", @@ -4476,7 +4553,8 @@ "type": "github" } ], - "time": "2025-02-07T04:58:17+00:00" + "time": "2025-02-07T04:58:17+00:00", + "published-time": "2025-02-07T04:58:28+00:00" }, { "name": "sebastian/recursion-context", @@ -4552,7 +4630,8 @@ "type": "tidelift" } ], - "time": "2025-08-13T04:44:59+00:00" + "time": "2025-08-13T04:44:59+00:00", + "published-time": "2025-08-13T04:45:10+00:00" }, { "name": "sebastian/type", @@ -4621,20 +4700,21 @@ "type": "tidelift" } ], - "time": "2026-05-20T06:45:45+00:00" + "time": "2026-05-20T06:45:45+00:00", + "published-time": "2026-05-20T06:50:13+00:00" }, { "name": "sebastian/version", - "version": "6.0.0", + "version": "6.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/version.git", - "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c" + "reference": "9e935a16187e14f39b0e79910dabd614972cdb26" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/3e6ccf7657d4f0a59200564b08cead899313b53c", - "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/9e935a16187e14f39b0e79910dabd614972cdb26", + "reference": "9e935a16187e14f39b0e79910dabd614972cdb26", "shasum": "" }, "require": { @@ -4667,15 +4747,28 @@ "support": { "issues": "https://github.com/sebastianbergmann/version/issues", "security": "https://github.com/sebastianbergmann/version/security/policy", - "source": "https://github.com/sebastianbergmann/version/tree/6.0.0" + "source": "https://github.com/sebastianbergmann/version/tree/6.0.1" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/version", + "type": "tidelift" } ], - "time": "2025-02-07T05:00:38+00:00" + "time": "2026-10-04T05:50:08+00:00", + "published-time": "2026-10-04T05:50:36+00:00" }, { "name": "staabm/side-effects-detector", @@ -4727,7 +4820,8 @@ "type": "github" } ], - "time": "2024-10-20T05:08:20+00:00" + "time": "2024-10-20T05:08:20+00:00", + "published-time": "2024-10-20T05:09:39+00:00" }, { "name": "symfony/polyfill-php80", @@ -4811,7 +4905,8 @@ "type": "tidelift" } ], - "time": "2026-09-25T15:50:05+00:00" + "time": "2026-09-25T15:50:05+00:00", + "published-time": "2026-09-30T10:23:35+00:00" }, { "name": "symfony/polyfill-php82", @@ -4891,7 +4986,8 @@ "type": "tidelift" } ], - "time": "2026-09-25T15:50:05+00:00" + "time": "2026-09-25T15:50:05+00:00", + "published-time": "2026-09-30T10:23:32+00:00" }, { "name": "theseer/tokenizer", @@ -4941,7 +5037,8 @@ "type": "github" } ], - "time": "2025-12-08T11:19:18+00:00" + "time": "2025-12-08T11:19:18+00:00", + "published-time": "2025-12-08T11:19:39+00:00" } ], "aliases": [], @@ -4958,5 +5055,5 @@ "platform-overrides": { "php": "8.3" }, - "plugin-api-version": "2.9.0" + "plugin-api-version": "2.11.0" } diff --git a/upload/api/vendor/composer/autoload_psr4.php b/upload/api/vendor/composer/autoload_psr4.php index 88daa3c9..3845fe79 100644 --- a/upload/api/vendor/composer/autoload_psr4.php +++ b/upload/api/vendor/composer/autoload_psr4.php @@ -27,6 +27,7 @@ 'Psr\\Clock\\' => array($vendorDir . '/psr/clock/src'), 'PhpParser\\' => array($vendorDir . '/nikic/php-parser/lib/PhpParser'), 'PHPStan\\PhpDocParser\\' => array($vendorDir . '/phpstan/phpdoc-parser/src'), + 'OpenApi\\Rector\\' => array($vendorDir . '/zircote/swagger-php/rector/src'), 'OpenApi\\' => array($vendorDir . '/zircote/swagger-php/src'), 'Monolog\\' => array($vendorDir . '/monolog/monolog/src/Monolog'), 'League\\Uri\\' => array($vendorDir . '/league/uri', $vendorDir . '/league/uri-interfaces'), diff --git a/upload/api/vendor/composer/autoload_static.php b/upload/api/vendor/composer/autoload_static.php index 820eb758..cba65828 100644 --- a/upload/api/vendor/composer/autoload_static.php +++ b/upload/api/vendor/composer/autoload_static.php @@ -51,6 +51,7 @@ class ComposerStaticInite77d48595e9f9bb29fb530f5885e1fc3 ), 'O' => array ( + 'OpenApi\\Rector\\' => 15, 'OpenApi\\' => 8, ), 'M' => @@ -169,6 +170,10 @@ class ComposerStaticInite77d48595e9f9bb29fb530f5885e1fc3 array ( 0 => __DIR__ . '/..' . '/phpstan/phpdoc-parser/src', ), + 'OpenApi\\Rector\\' => + array ( + 0 => __DIR__ . '/..' . '/zircote/swagger-php/rector/src', + ), 'OpenApi\\' => array ( 0 => __DIR__ . '/..' . '/zircote/swagger-php/src', diff --git a/upload/api/vendor/composer/installed.json b/upload/api/vendor/composer/installed.json index 5c870950..643285bc 100644 --- a/upload/api/vendor/composer/installed.json +++ b/upload/api/vendor/composer/installed.json @@ -25,6 +25,7 @@ "yoast/phpunit-polyfills": "^2.0.0" }, "time": "2023-06-19T06:10:36+00:00", + "published-time": "2023-06-19T06:16:55+00:00", "bin": [ "bin/generate-defuse-key" ], @@ -92,6 +93,7 @@ "psr/http-message": "The package containing the PSR-7 interfaces" }, "time": "2020-11-24T22:02:12+00:00", + "published-time": "2020-11-24T22:02:59+00:00", "type": "library", "extra": { "branch-alias": { @@ -155,6 +157,7 @@ "symfony/var-dumper": "^6.2.0|^7.0.0|^8.0.0" }, "time": "2026-09-22T14:32:34+00:00", + "published-time": "2026-09-22T14:33:13+00:00", "type": "library", "extra": { "branch-alias": { @@ -195,17 +198,17 @@ }, { "name": "lcobucci/jwt", - "version": "5.6.0", - "version_normalized": "5.6.0.0", + "version": "5.6.1", + "version_normalized": "5.6.1.0", "source": { "type": "git", "url": "https://github.com/lcobucci/jwt.git", - "reference": "bb3e9f21e4196e8afc41def81ef649c164bca25e" + "reference": "482760068e5a55a9045feddb3e2fc3f878cbd577" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/lcobucci/jwt/zipball/bb3e9f21e4196e8afc41def81ef649c164bca25e", - "reference": "bb3e9f21e4196e8afc41def81ef649c164bca25e", + "url": "https://api.github.com/repos/lcobucci/jwt/zipball/482760068e5a55a9045feddb3e2fc3f878cbd577", + "reference": "482760068e5a55a9045feddb3e2fc3f878cbd577", "shasum": "" }, "require": { @@ -229,7 +232,8 @@ "suggest": { "lcobucci/clock": ">= 3.2" }, - "time": "2025-10-17T11:30:53+00:00", + "time": "2026-10-10T09:42:14+00:00", + "published-time": "2026-10-10T09:43:25+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -255,7 +259,7 @@ ], "support": { "issues": "https://github.com/lcobucci/jwt/issues", - "source": "https://github.com/lcobucci/jwt/tree/5.6.0" + "source": "https://github.com/lcobucci/jwt/tree/5.6.1" }, "funding": [ { @@ -297,6 +301,7 @@ "phpunit/phpunit": "^8.5" }, "time": "2024-09-04T16:06:53+00:00", + "published-time": "2024-09-04T20:43:40+00:00", "type": "library", "extra": { "branch-alias": { @@ -377,6 +382,7 @@ "squizlabs/php_codesniffer": "^4.0" }, "time": "2026-06-25T15:24:07+00:00", + "published-time": "2026-06-25T15:28:46+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -433,21 +439,22 @@ }, { "name": "league/uri", - "version": "7.8.1", - "version_normalized": "7.8.1.0", + "version": "7.9.1", + "version_normalized": "7.9.1.0", "source": { "type": "git", "url": "https://github.com/thephpleague/uri.git", - "reference": "08cf38e3924d4f56238125547b5720496fac8fd4" + "reference": "40403f5c3141457a61f7f1aba6706e8fa4a0ece5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri/zipball/08cf38e3924d4f56238125547b5720496fac8fd4", - "reference": "08cf38e3924d4f56238125547b5720496fac8fd4", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/40403f5c3141457a61f7f1aba6706e8fa4a0ece5", + "reference": "40403f5c3141457a61f7f1aba6706e8fa4a0ece5", "shasum": "" }, "require": { - "league/uri-interfaces": "^7.8.1", + "ext-mbstring": "*", + "league/uri-interfaces": "^7.9.1", "php": "^8.1", "psr/http-factory": "^1" }, @@ -468,7 +475,8 @@ "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, - "time": "2026-03-15T20:22:25+00:00", + "time": "2026-10-27T07:14:23+00:00", + "published-time": "2026-10-28T08:38:15+00:00", "type": "library", "extra": { "branch-alias": { @@ -522,7 +530,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri/tree/7.8.1" + "source": "https://github.com/thephpleague/uri/tree/7.9.1" }, "funding": [ { @@ -534,17 +542,17 @@ }, { "name": "league/uri-interfaces", - "version": "7.8.1", - "version_normalized": "7.8.1.0", + "version": "7.9.1", + "version_normalized": "7.9.1.0", "source": { "type": "git", "url": "https://github.com/thephpleague/uri-interfaces.git", - "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928" + "reference": "994300a79e17f179112dc259f8df0c4de982f736" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/85d5c77c5d6d3af6c54db4a78246364908f3c928", - "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/994300a79e17f179112dc259f8df0c4de982f736", + "reference": "994300a79e17f179112dc259f8df0c4de982f736", "shasum": "" }, "require": { @@ -560,7 +568,8 @@ "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, - "time": "2026-03-08T20:05:35+00:00", + "time": "2026-10-27T07:14:23+00:00", + "published-time": "2026-10-28T08:38:21+00:00", "type": "library", "extra": { "branch-alias": { @@ -609,7 +618,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.1" + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.9.1" }, "funding": [ { @@ -681,6 +690,7 @@ "ruflin/elastica": "Allow sending log messages to an Elastic Search server" }, "time": "2026-09-29T19:21:12+00:00", + "published-time": "2026-09-29T19:21:44+00:00", "type": "library", "extra": { "branch-alias": { @@ -749,6 +759,7 @@ "phpunit/phpunit": "^4.8.35|~5.7" }, "time": "2026-07-09T19:38:47+00:00", + "published-time": "2026-07-09T19:39:54+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -805,6 +816,7 @@ "phpunit/phpunit": "^9.0" }, "time": "2026-09-13T18:51:52+00:00", + "published-time": "2026-09-13T18:53:16+00:00", "bin": [ "bin/php-parse" ], @@ -866,6 +878,7 @@ "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." }, "time": "2020-10-15T08:29:30+00:00", + "published-time": "2020-10-15T08:34:16+00:00", "type": "library", "installation-source": "dist", "notification-url": "https://packagist.org/downloads/", @@ -918,6 +931,7 @@ "phpunit/phpunit": "^9.0 || ^10 || ^11 || ^12" }, "time": "2025-08-30T10:22:22+00:00", + "published-time": "2025-08-30T10:22:43+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -986,6 +1000,7 @@ "friendsofphp/proxy-manager-lts": "Install it if you want to use lazy injection (version ^1)" }, "time": "2025-08-16T11:10:48+00:00", + "published-time": "2025-08-16T11:11:08+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -1029,17 +1044,17 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "2.3.6", - "version_normalized": "2.3.6.0", + "version": "2.4.1", + "version_normalized": "2.4.1.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "1427af4647235b4a73b13940b1f1258d3584f36e" + "reference": "af8e840551031d254cfdb3a18a62c3470a309c10" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/1427af4647235b4a73b13940b1f1258d3584f36e", - "reference": "1427af4647235b4a73b13940b1f1258d3584f36e", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/af8e840551031d254cfdb3a18a62c3470a309c10", + "reference": "af8e840551031d254cfdb3a18a62c3470a309c10", "shasum": "" }, "require": { @@ -1056,7 +1071,8 @@ "phpunit/phpunit": "^9.6", "symfony/process": "^5.2" }, - "time": "2026-09-27T20:07:45+00:00", + "time": "2026-10-30T19:36:28+00:00", + "published-time": "2026-10-30T19:37:11+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -1073,7 +1089,7 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.6" + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.4.1" }, "install-path": "../phpstan/phpdoc-parser" }, @@ -1096,6 +1112,7 @@ "php": "^7.0 || ^8.0" }, "time": "2022-11-25T14:36:26+00:00", + "published-time": "2022-11-25T14:59:22+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -1147,6 +1164,7 @@ "php": ">=7.4.0" }, "time": "2021-11-05T16:47:00+00:00", + "published-time": "2021-11-05T16:53:27+00:00", "type": "library", "extra": { "branch-alias": { @@ -1203,6 +1221,7 @@ "php": ">=7.2.0" }, "time": "2019-01-08T18:20:26+00:00", + "published-time": "2019-03-25T15:31:44+00:00", "type": "library", "extra": { "branch-alias": { @@ -1257,6 +1276,7 @@ "psr/http-message": "^1.0 || ^2.0" }, "time": "2024-04-15T12:06:14+00:00", + "published-time": "2024-05-10T07:32:19+00:00", "type": "library", "extra": { "branch-alias": { @@ -1314,6 +1334,7 @@ "php": "^7.2 || ^8.0" }, "time": "2023-04-04T09:54:51+00:00", + "published-time": "2023-04-04T09:55:35+00:00", "type": "library", "extra": { "branch-alias": { @@ -1371,6 +1392,7 @@ "psr/http-message": "^1.0 || ^2.0" }, "time": "2023-04-10T20:06:20+00:00", + "published-time": "2023-04-10T20:07:30+00:00", "type": "library", "extra": { "branch-alias": { @@ -1431,6 +1453,7 @@ "psr/http-server-handler": "^1.0" }, "time": "2023-04-11T06:14:47+00:00", + "published-time": "2023-04-11T06:15:22+00:00", "type": "library", "extra": { "branch-alias": { @@ -1489,6 +1512,7 @@ "php": ">=8.0.0" }, "time": "2024-09-11T13:17:53+00:00", + "published-time": "2024-09-11T13:21:14+00:00", "type": "library", "extra": { "branch-alias": { @@ -1525,17 +1549,17 @@ }, { "name": "radebatz/type-info-extras", - "version": "1.0.9", - "version_normalized": "1.0.9.0", + "version": "1.0.11", + "version_normalized": "1.0.11.0", "source": { "type": "git", "url": "https://github.com/DerManoMann/type-info-extras.git", - "reference": "2d9f01d60d642f890d9f170c0d8fde1f75ab17fb" + "reference": "1436aa4c9aec0af093a27c2287900a3008535f32" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/DerManoMann/type-info-extras/zipball/2d9f01d60d642f890d9f170c0d8fde1f75ab17fb", - "reference": "2d9f01d60d642f890d9f170c0d8fde1f75ab17fb", + "url": "https://api.github.com/repos/DerManoMann/type-info-extras/zipball/1436aa4c9aec0af093a27c2287900a3008535f32", + "reference": "1436aa4c9aec0af093a27c2287900a3008535f32", "shasum": "" }, "require": { @@ -1548,7 +1572,8 @@ "phpstan/phpstan": "^2.1", "phpunit/phpunit": "^11.0" }, - "time": "2026-09-29T22:04:31+00:00", + "time": "2026-10-26T22:24:45+00:00", + "published-time": "2026-10-26T22:25:56+00:00", "type": "library", "extra": { "branch-alias": { @@ -1584,7 +1609,7 @@ ], "support": { "issues": "https://github.com/DerManoMann/type-info-extras/issues", - "source": "https://github.com/DerManoMann/type-info-extras/tree/1.0.9" + "source": "https://github.com/DerManoMann/type-info-extras/tree/1.0.11" }, "install-path": "../radebatz/type-info-extras" }, @@ -1611,6 +1636,7 @@ "phpunit/phpunit": "^5 || ^6.5" }, "time": "2019-03-08T08:55:37+00:00", + "published-time": "2019-03-08T09:01:32+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -1671,6 +1697,7 @@ "squizlabs/php_codesniffer": "^3.13" }, "time": "2025-11-02T17:51:19+00:00", + "published-time": "2025-11-05T19:46:07+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -1768,6 +1795,7 @@ "slim/psr7": "Slim PSR-7 implementation. See https://www.slimframework.com/docs/v4/start/installation.html for more information." }, "time": "2026-09-01T17:11:05+00:00", + "published-time": "2026-09-01T17:13:05+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -1838,17 +1866,17 @@ }, { "name": "symfony/console", - "version": "v7.4.20", - "version_normalized": "7.4.20.0", + "version": "v7.4.22", + "version_normalized": "7.4.22.0", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "d7bca2fe05241dc7732d0c8b6d551b879f5d0a7d" + "reference": "8012f842110b414fd1cc641adb0ed8423353b0bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/d7bca2fe05241dc7732d0c8b6d551b879f5d0a7d", - "reference": "d7bca2fe05241dc7732d0c8b6d551b879f5d0a7d", + "url": "https://api.github.com/repos/symfony/console/zipball/8012f842110b414fd1cc641adb0ed8423353b0bb", + "reference": "8012f842110b414fd1cc641adb0ed8423353b0bb", "shasum": "" }, "require": { @@ -1881,7 +1909,8 @@ "symfony/stopwatch": "^6.4|^7.0|^8.0", "symfony/var-dumper": "^6.4|^7.0|^8.0" }, - "time": "2026-09-18T15:55:00+00:00", + "time": "2026-10-28T11:08:27+00:00", + "published-time": "2026-10-28T18:21:33+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -1915,7 +1944,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.4.20" + "source": "https://github.com/symfony/console/tree/v7.4.22" }, "funding": [ { @@ -1956,6 +1985,7 @@ "php": ">=8.1" }, "time": "2026-06-05T06:23:12+00:00", + "published-time": "2026-06-27T21:46:34+00:00", "type": "library", "extra": { "thanks": { @@ -2013,17 +2043,17 @@ }, { "name": "symfony/finder", - "version": "v7.4.20", - "version_normalized": "7.4.20.0", + "version": "v7.4.22", + "version_normalized": "7.4.22.0", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "2dd31e5208db7a0fda7bfc998ad727b79c2e4d1e" + "reference": "e13420a8e0a76bdde1e7be09f923e62ad13739f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/2dd31e5208db7a0fda7bfc998ad727b79c2e4d1e", - "reference": "2dd31e5208db7a0fda7bfc998ad727b79c2e4d1e", + "url": "https://api.github.com/repos/symfony/finder/zipball/e13420a8e0a76bdde1e7be09f923e62ad13739f2", + "reference": "e13420a8e0a76bdde1e7be09f923e62ad13739f2", "shasum": "" }, "require": { @@ -2032,7 +2062,8 @@ "require-dev": { "symfony/filesystem": "^6.4|^7.0|^8.0" }, - "time": "2026-09-26T06:31:10+00:00", + "time": "2026-10-19T16:56:54+00:00", + "published-time": "2026-10-28T18:21:14+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -2060,7 +2091,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.4.20" + "source": "https://github.com/symfony/finder/tree/v7.4.22" }, "funding": [ { @@ -2107,6 +2138,7 @@ "ext-ctype": "For best performance" }, "time": "2026-04-10T16:19:22+00:00", + "published-time": "2026-04-26T14:44:50+00:00", "type": "library", "extra": { "thanks": { @@ -2170,17 +2202,17 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.43.0", - "version_normalized": "1.43.0.0", + "version": "v1.43.3", + "version_normalized": "1.43.3.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "5cac8f4db97208823aa353f59a01faad2da52c75" + "reference": "5a8f33c87aeecffce24e721b5064fab05f4becf8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/5cac8f4db97208823aa353f59a01faad2da52c75", - "reference": "5cac8f4db97208823aa353f59a01faad2da52c75", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/5a8f33c87aeecffce24e721b5064fab05f4becf8", + "reference": "5a8f33c87aeecffce24e721b5064fab05f4becf8", "shasum": "" }, "require": { @@ -2189,7 +2221,8 @@ "suggest": { "ext-intl": "For best performance" }, - "time": "2026-09-25T15:46:47+00:00", + "time": "2026-10-15T09:18:53+00:00", + "published-time": "2026-10-28T10:07:44+00:00", "type": "library", "extra": { "thanks": { @@ -2231,7 +2264,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.43.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.43.3" }, "funding": [ { @@ -2275,6 +2308,7 @@ "ext-intl": "For best performance" }, "time": "2026-09-23T22:02:09+00:00", + "published-time": "2026-09-30T10:23:47+00:00", "type": "library", "extra": { "thanks": { @@ -2343,17 +2377,17 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.43.0", - "version_normalized": "1.43.0.0", + "version": "v1.43.3", + "version_normalized": "1.43.3.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "4d5d331bf731e0aedbc6b2f5e61135b7b7996a72" + "reference": "122889ed24dc000165bcbfd77ec412f15cbdfc6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/4d5d331bf731e0aedbc6b2f5e61135b7b7996a72", - "reference": "4d5d331bf731e0aedbc6b2f5e61135b7b7996a72", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/122889ed24dc000165bcbfd77ec412f15cbdfc6a", + "reference": "122889ed24dc000165bcbfd77ec412f15cbdfc6a", "shasum": "" }, "require": { @@ -2366,7 +2400,8 @@ "suggest": { "ext-mbstring": "For best performance" }, - "time": "2026-09-24T15:19:04+00:00", + "time": "2026-10-28T09:35:49+00:00", + "published-time": "2026-10-28T10:07:45+00:00", "type": "library", "extra": { "thanks": { @@ -2407,7 +2442,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.43.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.43.3" }, "funding": [ { @@ -2453,6 +2488,7 @@ "ext-psr": "<1.1|>=2" }, "time": "2026-07-27T15:39:01+00:00", + "published-time": "2026-08-30T19:56:37+00:00", "type": "library", "extra": { "thanks": { @@ -2521,17 +2557,17 @@ }, { "name": "symfony/string", - "version": "v7.4.19", - "version_normalized": "7.4.19.0", + "version": "v7.4.22", + "version_normalized": "7.4.22.0", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "02731ce2bd26514bec9efc0baef2cd0b0cf67f46" + "reference": "8dff7da3a56638f1d189c5368183294102de4ced" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/02731ce2bd26514bec9efc0baef2cd0b0cf67f46", - "reference": "02731ce2bd26514bec9efc0baef2cd0b0cf67f46", + "url": "https://api.github.com/repos/symfony/string/zipball/8dff7da3a56638f1d189c5368183294102de4ced", + "reference": "8dff7da3a56638f1d189c5368183294102de4ced", "shasum": "" }, "require": { @@ -2552,7 +2588,8 @@ "symfony/translation-contracts": "^2.5|^3.0", "symfony/var-exporter": "^6.4|^7.0|^8.0" }, - "time": "2026-09-11T14:46:58+00:00", + "time": "2026-10-23T16:51:52+00:00", + "published-time": "2026-10-28T18:21:18+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -2591,7 +2628,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.4.19" + "source": "https://github.com/symfony/string/tree/v7.4.22" }, "funding": [ { @@ -2615,17 +2652,17 @@ }, { "name": "symfony/type-info", - "version": "v7.4.20", - "version_normalized": "7.4.20.0", + "version": "v7.4.22", + "version_normalized": "7.4.22.0", "source": { "type": "git", "url": "https://github.com/symfony/type-info.git", - "reference": "bbaaf3366400f5c85f66800b955c77b46e8abad2" + "reference": "f69ab198af73e1aac485c0ff5aa066d20c14adf0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/type-info/zipball/bbaaf3366400f5c85f66800b955c77b46e8abad2", - "reference": "bbaaf3366400f5c85f66800b955c77b46e8abad2", + "url": "https://api.github.com/repos/symfony/type-info/zipball/f69ab198af73e1aac485c0ff5aa066d20c14adf0", + "reference": "f69ab198af73e1aac485c0ff5aa066d20c14adf0", "shasum": "" }, "require": { @@ -2639,7 +2676,8 @@ "require-dev": { "phpstan/phpdoc-parser": "^1.30|^2.0" }, - "time": "2026-09-28T12:19:37+00:00", + "time": "2026-10-24T08:22:23+00:00", + "published-time": "2026-10-28T18:20:01+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -2677,7 +2715,7 @@ "type" ], "support": { - "source": "https://github.com/symfony/type-info/tree/v7.4.20" + "source": "https://github.com/symfony/type-info/tree/v7.4.22" }, "funding": [ { @@ -2701,17 +2739,17 @@ }, { "name": "symfony/yaml", - "version": "v7.4.20", - "version_normalized": "7.4.20.0", + "version": "v7.4.22", + "version_normalized": "7.4.22.0", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "0222bf4dfaa1e3ceb8fff579635d655f2950d5a7" + "reference": "1ed6afa13d48c579c51cbf32e38f50686313497b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/0222bf4dfaa1e3ceb8fff579635d655f2950d5a7", - "reference": "0222bf4dfaa1e3ceb8fff579635d655f2950d5a7", + "url": "https://api.github.com/repos/symfony/yaml/zipball/1ed6afa13d48c579c51cbf32e38f50686313497b", + "reference": "1ed6afa13d48c579c51cbf32e38f50686313497b", "shasum": "" }, "require": { @@ -2725,7 +2763,8 @@ "require-dev": { "symfony/console": "^6.4|^7.0|^8.0" }, - "time": "2026-09-26T06:31:10+00:00", + "time": "2026-10-18T14:39:27+00:00", + "published-time": "2026-10-28T18:20:33+00:00", "bin": [ "Resources/bin/yaml-lint" ], @@ -2756,7 +2795,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.4.20" + "source": "https://github.com/symfony/yaml/tree/v7.4.22" }, "funding": [ { @@ -2780,17 +2819,17 @@ }, { "name": "zircote/swagger-php", - "version": "6.11.0", - "version_normalized": "6.11.0.0", + "version": "6.20.1", + "version_normalized": "6.20.1.0", "source": { "type": "git", "url": "https://github.com/zircote/swagger-php.git", - "reference": "f998f7e712658fba61f86fccbdcf8e301a982175" + "reference": "34f1bcc414012cf0ffbc747b0dc418bb67090c2f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zircote/swagger-php/zipball/f998f7e712658fba61f86fccbdcf8e301a982175", - "reference": "f998f7e712658fba61f86fccbdcf8e301a982175", + "url": "https://api.github.com/repos/zircote/swagger-php/zipball/34f1bcc414012cf0ffbc747b0dc418bb67090c2f", + "reference": "34f1bcc414012cf0ffbc747b0dc418bb67090c2f", "shasum": "" }, "require": { @@ -2799,22 +2838,26 @@ "phpstan/phpdoc-parser": "^2.0", "psr/log": "^1.1 || ^2.0 || ^3.0", "radebatz/type-info-extras": "^1.0.2", - "symfony/console": "^6.4 || ^7.0 || ^8.0", + "symfony/console": "^5.4 || ^6.4 || ^7.0 || ^8.0", "symfony/deprecation-contracts": "^2 || ^3", "symfony/finder": "^5.0 || ^6.0 || ^7.0 || ^8.0", "symfony/yaml": "^5.4 || ^6.0 || ^7.0 || ^8.0" }, "conflict": { - "symfony/process": ">=6, <6.4.14" + "symfony/console": "<5.4.35", + "symfony/finder": "<5.3.7", + "symfony/process": ">=6, <6.4.14", + "symfony/string": "<5.4.41" }, "require-dev": { "doctrine/annotations": "^2.0", - "friendsofphp/php-cs-fixer": "3.95.23", - "phpstan/phpstan": "2.2.12", + "friendsofphp/php-cs-fixer": "3.95.27", + "phpstan/phpstan": "2.3.1", "phpunit/phpunit": "^11.5 || >=12.5.22", - "rector/rector": "2.6.5" + "rector/rector": "2.7.0" }, - "time": "2026-09-26T07:11:30+00:00", + "time": "2026-10-30T21:52:05+00:00", + "published-time": "2026-10-30T21:54:53+00:00", "bin": [ "bin/openapi" ], @@ -2827,7 +2870,8 @@ "installation-source": "dist", "autoload": { "psr-4": { - "OpenApi\\": "src" + "OpenApi\\": "src", + "OpenApi\\Rector\\": "rector/src" } }, "notification-url": "https://packagist.org/downloads/", @@ -2860,7 +2904,7 @@ ], "support": { "issues": "https://github.com/zircote/swagger-php/issues", - "source": "https://github.com/zircote/swagger-php/tree/6.11.0" + "source": "https://github.com/zircote/swagger-php/tree/6.20.1" }, "funding": [ { diff --git a/upload/api/vendor/composer/installed.php b/upload/api/vendor/composer/installed.php index c3e1f4be..f41fb376 100644 --- a/upload/api/vendor/composer/installed.php +++ b/upload/api/vendor/composer/installed.php @@ -3,7 +3,7 @@ 'name' => 'devcraftclub/dle-api', 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => '60ce1eb63d1c0b6bba18e102247593b97e8ef108', + 'reference' => '34899d886009fecb59f615754a6345ab3723f36c', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -22,7 +22,7 @@ 'devcraftclub/dle-api' => array( 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => '60ce1eb63d1c0b6bba18e102247593b97e8ef108', + 'reference' => '34899d886009fecb59f615754a6345ab3723f36c', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -47,9 +47,9 @@ 'dev_requirement' => false, ), 'lcobucci/jwt' => array( - 'pretty_version' => '5.6.0', - 'version' => '5.6.0.0', - 'reference' => 'bb3e9f21e4196e8afc41def81ef649c164bca25e', + 'pretty_version' => '5.6.1', + 'version' => '5.6.1.0', + 'reference' => '482760068e5a55a9045feddb3e2fc3f878cbd577', 'type' => 'library', 'install_path' => __DIR__ . '/../lcobucci/jwt', 'aliases' => array(), @@ -80,18 +80,18 @@ ), ), 'league/uri' => array( - 'pretty_version' => '7.8.1', - 'version' => '7.8.1.0', - 'reference' => '08cf38e3924d4f56238125547b5720496fac8fd4', + 'pretty_version' => '7.9.1', + 'version' => '7.9.1.0', + 'reference' => '40403f5c3141457a61f7f1aba6706e8fa4a0ece5', 'type' => 'library', 'install_path' => __DIR__ . '/../league/uri', 'aliases' => array(), 'dev_requirement' => false, ), 'league/uri-interfaces' => array( - 'pretty_version' => '7.8.1', - 'version' => '7.8.1.0', - 'reference' => '85d5c77c5d6d3af6c54db4a78246364908f3c928', + 'pretty_version' => '7.9.1', + 'version' => '7.9.1.0', + 'reference' => '994300a79e17f179112dc259f8df0c4de982f736', 'type' => 'library', 'install_path' => __DIR__ . '/../league/uri-interfaces', 'aliases' => array(), @@ -158,9 +158,9 @@ 'dev_requirement' => false, ), 'phpstan/phpdoc-parser' => array( - 'pretty_version' => '2.3.6', - 'version' => '2.3.6.0', - 'reference' => '1427af4647235b4a73b13940b1f1258d3584f36e', + 'pretty_version' => '2.4.1', + 'version' => '2.4.1.0', + 'reference' => 'af8e840551031d254cfdb3a18a62c3470a309c10', 'type' => 'library', 'install_path' => __DIR__ . '/../phpstan/phpdoc-parser', 'aliases' => array(), @@ -270,9 +270,9 @@ ), ), 'radebatz/type-info-extras' => array( - 'pretty_version' => '1.0.9', - 'version' => '1.0.9.0', - 'reference' => '2d9f01d60d642f890d9f170c0d8fde1f75ab17fb', + 'pretty_version' => '1.0.11', + 'version' => '1.0.11.0', + 'reference' => '1436aa4c9aec0af093a27c2287900a3008535f32', 'type' => 'library', 'install_path' => __DIR__ . '/../radebatz/type-info-extras', 'aliases' => array(), @@ -306,9 +306,9 @@ 'dev_requirement' => false, ), 'symfony/console' => array( - 'pretty_version' => 'v7.4.20', - 'version' => '7.4.20.0', - 'reference' => 'd7bca2fe05241dc7732d0c8b6d551b879f5d0a7d', + 'pretty_version' => 'v7.4.22', + 'version' => '7.4.22.0', + 'reference' => '8012f842110b414fd1cc641adb0ed8423353b0bb', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/console', 'aliases' => array(), @@ -324,9 +324,9 @@ 'dev_requirement' => false, ), 'symfony/finder' => array( - 'pretty_version' => 'v7.4.20', - 'version' => '7.4.20.0', - 'reference' => '2dd31e5208db7a0fda7bfc998ad727b79c2e4d1e', + 'pretty_version' => 'v7.4.22', + 'version' => '7.4.22.0', + 'reference' => 'e13420a8e0a76bdde1e7be09f923e62ad13739f2', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/finder', 'aliases' => array(), @@ -342,9 +342,9 @@ 'dev_requirement' => false, ), 'symfony/polyfill-intl-grapheme' => array( - 'pretty_version' => 'v1.43.0', - 'version' => '1.43.0.0', - 'reference' => '5cac8f4db97208823aa353f59a01faad2da52c75', + 'pretty_version' => 'v1.43.3', + 'version' => '1.43.3.0', + 'reference' => '5a8f33c87aeecffce24e721b5064fab05f4becf8', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/polyfill-intl-grapheme', 'aliases' => array(), @@ -360,9 +360,9 @@ 'dev_requirement' => false, ), 'symfony/polyfill-mbstring' => array( - 'pretty_version' => 'v1.43.0', - 'version' => '1.43.0.0', - 'reference' => '4d5d331bf731e0aedbc6b2f5e61135b7b7996a72', + 'pretty_version' => 'v1.43.3', + 'version' => '1.43.3.0', + 'reference' => '122889ed24dc000165bcbfd77ec412f15cbdfc6a', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/polyfill-mbstring', 'aliases' => array(), @@ -378,36 +378,36 @@ 'dev_requirement' => false, ), 'symfony/string' => array( - 'pretty_version' => 'v7.4.19', - 'version' => '7.4.19.0', - 'reference' => '02731ce2bd26514bec9efc0baef2cd0b0cf67f46', + 'pretty_version' => 'v7.4.22', + 'version' => '7.4.22.0', + 'reference' => '8dff7da3a56638f1d189c5368183294102de4ced', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/string', 'aliases' => array(), 'dev_requirement' => false, ), 'symfony/type-info' => array( - 'pretty_version' => 'v7.4.20', - 'version' => '7.4.20.0', - 'reference' => 'bbaaf3366400f5c85f66800b955c77b46e8abad2', + 'pretty_version' => 'v7.4.22', + 'version' => '7.4.22.0', + 'reference' => 'f69ab198af73e1aac485c0ff5aa066d20c14adf0', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/type-info', 'aliases' => array(), 'dev_requirement' => false, ), 'symfony/yaml' => array( - 'pretty_version' => 'v7.4.20', - 'version' => '7.4.20.0', - 'reference' => '0222bf4dfaa1e3ceb8fff579635d655f2950d5a7', + 'pretty_version' => 'v7.4.22', + 'version' => '7.4.22.0', + 'reference' => '1ed6afa13d48c579c51cbf32e38f50686313497b', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/yaml', 'aliases' => array(), 'dev_requirement' => false, ), 'zircote/swagger-php' => array( - 'pretty_version' => '6.11.0', - 'version' => '6.11.0.0', - 'reference' => 'f998f7e712658fba61f86fccbdcf8e301a982175', + 'pretty_version' => '6.20.1', + 'version' => '6.20.1.0', + 'reference' => '34f1bcc414012cf0ffbc747b0dc418bb67090c2f', 'type' => 'library', 'install_path' => __DIR__ . '/../zircote/swagger-php', 'aliases' => array(), diff --git a/upload/api/vendor/lcobucci/jwt/src/Signer/Ecdsa.php b/upload/api/vendor/lcobucci/jwt/src/Signer/Ecdsa.php index d6f8e1f4..b29eb32b 100644 --- a/upload/api/vendor/lcobucci/jwt/src/Signer/Ecdsa.php +++ b/upload/api/vendor/lcobucci/jwt/src/Signer/Ecdsa.php @@ -3,6 +3,7 @@ namespace Lcobucci\JWT\Signer; +use Lcobucci\JWT\Signer\Ecdsa\ConversionFailed; use Lcobucci\JWT\Signer\Ecdsa\MultibyteStringConverter; use Lcobucci\JWT\Signer\Ecdsa\SignatureConverter; @@ -25,11 +26,13 @@ final public function sign(string $payload, Key $key): string final public function verify(string $expected, string $payload, Key $key): bool { - return $this->verifySignature( - $this->converter->toAsn1($expected, $this->pointLength()), - $payload, - $key->contents(), - ); + try { + $asn1Signature = $this->converter->toAsn1($expected, $this->pointLength()); + } catch (ConversionFailed) { + return false; + } + + return $this->verifySignature($asn1Signature, $payload, $key->contents()); } /** {@inheritDoc} */ diff --git a/upload/api/vendor/lcobucci/jwt/src/Signer/Eddsa.php b/upload/api/vendor/lcobucci/jwt/src/Signer/Eddsa.php index 03ad88bd..0642a9db 100644 --- a/upload/api/vendor/lcobucci/jwt/src/Signer/Eddsa.php +++ b/upload/api/vendor/lcobucci/jwt/src/Signer/Eddsa.php @@ -8,6 +8,9 @@ use function sodium_crypto_sign_detached; use function sodium_crypto_sign_verify_detached; +use function strlen; + +use const SODIUM_CRYPTO_SIGN_BYTES; final class Eddsa implements Signer { @@ -27,6 +30,10 @@ public function sign(string $payload, Key $key): string public function verify(string $expected, string $payload, Key $key): bool { + if (strlen($expected) !== SODIUM_CRYPTO_SIGN_BYTES) { + return false; + } + try { return sodium_crypto_sign_verify_detached($expected, $payload, $key->contents()); } catch (SodiumException $sodiumException) { diff --git a/upload/api/vendor/lcobucci/jwt/src/Token/Parser.php b/upload/api/vendor/lcobucci/jwt/src/Token/Parser.php index 27db5404..15fc1da4 100644 --- a/upload/api/vendor/lcobucci/jwt/src/Token/Parser.php +++ b/upload/api/vendor/lcobucci/jwt/src/Token/Parser.php @@ -11,8 +11,10 @@ use function array_key_exists; use function count; use function explode; +use function get_debug_type; use function is_array; use function is_numeric; +use function is_string; use function number_format; final class Parser implements ParserInterface @@ -149,10 +151,12 @@ private function guardAgainstEmptyStringKeys(array $array, string $part): void } /** @throws InvalidTokenStructure */ - private function convertDate(int|float|string $timestamp): DateTimeImmutable + private function convertDate(mixed $timestamp): DateTimeImmutable { if (! is_numeric($timestamp)) { - throw InvalidTokenStructure::dateIsNotParseable($timestamp); + throw InvalidTokenStructure::dateIsNotParseable( + is_string($timestamp) ? $timestamp : get_debug_type($timestamp), + ); } $normalizedTimestamp = number_format((float) $timestamp, self::MICROSECOND_PRECISION, '.', ''); diff --git a/upload/api/vendor/league/uri-interfaces/Contracts/DomainHostInterface.php b/upload/api/vendor/league/uri-interfaces/Contracts/DomainHostInterface.php index a2f1745f..a0a8b908 100644 --- a/upload/api/vendor/league/uri-interfaces/Contracts/DomainHostInterface.php +++ b/upload/api/vendor/league/uri-interfaces/Contracts/DomainHostInterface.php @@ -15,10 +15,10 @@ use BackedEnum; use Countable; -use Iterator; use IteratorAggregate; use League\Uri\Exceptions\SyntaxError; use Stringable; +use Traversable; /** * @extends IteratorAggregate @@ -40,9 +40,9 @@ public function count(): int; /** * Iterate over the Domain labels. * - * @return Iterator + * @return Traversable */ - public function getIterator(): Iterator; + public function getIterator(): Traversable; /** * Retrieves a single host label. diff --git a/upload/api/vendor/league/uri-interfaces/Encoder.php b/upload/api/vendor/league/uri-interfaces/Encoder.php index cbd3f2e6..ea10edf4 100644 --- a/upload/api/vendor/league/uri-interfaces/Encoder.php +++ b/upload/api/vendor/league/uri-interfaces/Encoder.php @@ -25,7 +25,6 @@ use function explode; use function filter_var; use function gettype; -use function in_array; use function preg_match; use function preg_replace_callback; use function rawurldecode; @@ -42,24 +41,18 @@ final class Encoder { private const REGEXP_CHARS_INVALID = '/[\x00-\x1f\x7f]/'; private const REGEXP_CHARS_ENCODED = ',%[A-Fa-f0-9]{2},'; - private const REGEXP_CHARS_PREVENTS_DECODING = ',% - 2[A-F|1-2|4-9]| - 3[0-9|B|D]| - 4[1-9|A-F]| - 5[0-9|A|F]| - 6[1-9|A-F]| - 7[0-9|E] - ,ix'; + private const REGEXP_CHARS_PREVENTS_DECODING = ',%2[A-F1-24-9]|3[0-9BD]|4[1-9A-F]|5[0-9AF]|6[1-9A-F]|7[0-9E],i'; private const REGEXP_PART_SUBDELIM = "\!\$&'\(\)\*\+,;\=%"; private const REGEXP_PART_UNRESERVED = 'A-Za-z\d_\-.~'; private const REGEXP_PART_ENCODED = '%(?![A-Fa-f\d]{2})'; + private const RFC3986_RESERVED_CHARACTERS = ':/?#[]@!$&\'()*+,;='; /** * Unreserved characters. * * @see https://www.rfc-editor.org/rfc/rfc3986.html#section-2.3 */ - private const REGEXP_UNRESERVED_CHARACTERS = ',%(2[DdEe]|3[0-9]|4[1-9A-Fa-f]|5[AaFf]|6[1-9A-Fa-f]|7[0-9A-Ea-e]),'; + private const REGEXP_UNRESERVED_CHARACTERS = ',%(2[DdEe]|3[0-9]|4[1-9A-Fa-f]|5[0-9AaFf]|6[1-9A-Fa-f]|7[0-9AaEe]),'; /** * Tell whether the user component is correctly encoded. @@ -216,17 +209,14 @@ public static function decodeAll(BackedEnum|Stringable|string|null $component): } /** - * Decodes the URI component without decoding the unreserved characters which are already encoded. + * Decodes the URI component while preserving percent-encoded characters that must remain encoded. */ public static function decodeNecessary(BackedEnum|Stringable|string|int|null $component): ?string { - $decoder = static function (array $matches): string { - if (1 === preg_match(self::REGEXP_CHARS_PREVENTS_DECODING, $matches[0])) { - return strtoupper($matches[0]); - } - - return rawurldecode($matches[0]); - }; + $decoder = static fn (array $matches): string => + 1 === preg_match(self::REGEXP_CHARS_PREVENTS_DECODING, $matches[0]) + ? strtoupper($matches[0]) + : rawurldecode($matches[0]); return self::decode($component, $decoder); } @@ -282,13 +272,25 @@ public static function encodePath(BackedEnum|Stringable|string|null $component): */ public static function decodePath(BackedEnum|Stringable|string|null $path): ?string { - $decoder = static function (array $matches): string { - $encodedChar = strtoupper($matches[0]); + $path = self::filterComponent($path); + if (null === $path || '' === $path) { + return $path; + } - return in_array($encodedChar, ['%2F', '%20', '%3F', '%23'], true) ? $encodedChar : rawurldecode($encodedChar); - }; + 1 !== preg_match(self::REGEXP_CHARS_INVALID, $path) || throw new SyntaxError('Invalid component string: '.$path.'.'); - return self::decode($path, $decoder); + return (string) preg_replace_callback( + '/%(?!25[A-Fa-f0-9]{2})[A-Fa-f0-9]{2}/', + static function (array $matches): string { + $encodedChar = strtoupper($matches[0]); + $decodedChar = rawurldecode($encodedChar); + + return str_contains(self::RFC3986_RESERVED_CHARACTERS, $decodedChar) + ? $encodedChar + : $decodedChar; + }, + $path, + ); } /** @@ -319,15 +321,18 @@ public static function isQueryEncoded(BackedEnum|Stringable|string|null $encoded /** * Decodes the query component while preserving characters that should not be decoded in the context of a full valid URI. */ - public static function decodeQuery(BackedEnum|Stringable|string|null $path): ?string + public static function decodeQuery(BackedEnum|Stringable|string|null $query): ?string { $decoder = static function (array $matches): string { $encodedChar = strtoupper($matches[0]); + $decodedChar = rawurldecode($encodedChar); - return in_array($encodedChar, ['%26', '%3D', '%20', '%23', '%3F'], true) ? $encodedChar : rawurldecode($encodedChar); + return str_contains(self::RFC3986_RESERVED_CHARACTERS, $decodedChar) + ? $encodedChar + : $decodedChar; }; - return self::decode($path, $decoder); + return self::decode($query, $decoder); } /** @@ -359,9 +364,16 @@ public static function isFragmentEncoded(BackedEnum|Stringable|string|null $enco /** * Decodes the fragment component while preserving characters that should not be decoded in the context of a full valid URI. */ - public static function decodeFragment(BackedEnum|Stringable|string|null $path): ?string + public static function decodeFragment(BackedEnum|Stringable|string|null $fragment): ?string { - return self::decode($path, static fn (array $matches): string => '%20' === $matches[0] ? $matches[0] : rawurldecode($matches[0])); + return self::decode($fragment, static function (array $matches): string { + $encodedChar = strtoupper($matches[0]); + $decodedChar = rawurldecode($encodedChar); + + return str_contains(self::RFC3986_RESERVED_CHARACTERS, $decodedChar) + ? $encodedChar + : $decodedChar; + }); } /** @@ -407,7 +419,7 @@ public static function normalizeHost(BackedEnum|Stringable|string|null $host): ? return (!str_contains($host, '%')) ? $host : preg_replace_callback( '/%[a-f0-9]{2}/', - fn (array $matches) => 1 === preg_match('/%([0-7][0-9a-f])/', $matches[0]) ? rawurldecode($matches[0]) : strtoupper($matches[0]), + fn (array $matches) => 1 === preg_match('/%([0-7][0-9a-f])/', $matches[0]) ? strtolower(rawurldecode($matches[0])) : strtoupper($matches[0]), $host ); } @@ -424,7 +436,7 @@ public static function encodeQueryOrFragment(BackedEnum|Stringable|string|null $ return self::encode($component, $pattern); } - public static function encodeQueryKeyValue(mixed $component): ?string + public static function encodeQueryKeyValue(mixed $component): string { static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.']+|'.self::REGEXP_PART_ENCODED.'/'; $encoder = static fn (array $found): string => 1 === preg_match('/[^'.self::REGEXP_PART_UNRESERVED.']/', rawurldecode($found[0])) ? rawurlencode($found[0]) : $found[0]; @@ -476,15 +488,9 @@ private static function decode(BackedEnum|Stringable|string|int|null $component, return $component; } - if (1 === preg_match(self::REGEXP_CHARS_INVALID, $component)) { - throw new SyntaxError('Invalid component string: '.$component.'.'); - } - - if (1 === preg_match(self::REGEXP_CHARS_ENCODED, $component)) { - return (string) preg_replace_callback(self::REGEXP_CHARS_ENCODED, $decoder, $component); - } + 1 !== preg_match(self::REGEXP_CHARS_INVALID, $component) || throw new SyntaxError('Invalid component string: '.$component.'.'); - return $component; + return (string) preg_replace_callback(self::REGEXP_CHARS_ENCODED, $decoder, $component); } /** diff --git a/upload/api/vendor/league/uri-interfaces/HostRecord.php b/upload/api/vendor/league/uri-interfaces/HostRecord.php index e379568c..b9709d70 100644 --- a/upload/api/vendor/league/uri-interfaces/HostRecord.php +++ b/upload/api/vendor/league/uri-interfaces/HostRecord.php @@ -162,7 +162,13 @@ public function toUnicode(): ?string { if (!$this->unicodeIsLoaded) { $this->unicodeIsLoaded = true; - $this->hostAsUnicode = $this->isDomainType() && null !== $this->value ? IdnConverter::toUnicode($this->value)->domain() : $this->value; + $this->hostAsUnicode = $this->value; + if ($this->isDomainType() && null !== $this->value) { + $result = IdnConverter::toUnicode($this->value); + if (!$result->hasErrors()) { + $this->hostAsUnicode = $result->domain(); + } + } } return $this->hostAsUnicode; @@ -287,6 +293,15 @@ public static function isDomain(Stringable|string|null $host): bool } } + public static function parse(BackedEnum|Stringable|string|null $host): ?self + { + try { + return self::from($host); + } catch (Throwable) { + return null; + } + } + /** * @throws SyntaxError */ diff --git a/upload/api/vendor/league/uri-interfaces/IPv4/Converter.php b/upload/api/vendor/league/uri-interfaces/IPv4/Converter.php index 2db120d8..e625f421 100644 --- a/upload/api/vendor/league/uri-interfaces/IPv4/Converter.php +++ b/upload/api/vendor/league/uri-interfaces/IPv4/Converter.php @@ -148,7 +148,7 @@ public function toIPv6Using6to4(BackedEnum|Stringable|string|null $host): ?strin return null; } - /** @var array $parts */ + /** @var non-empty-list $parts */ $parts = array_map( fn (string $part): string => sprintf('%02x', $part), explode('.', $host) diff --git a/upload/api/vendor/league/uri-interfaces/IPv6/Converter.php b/upload/api/vendor/league/uri-interfaces/IPv6/Converter.php index 511a5ffb..75adc20c 100644 --- a/upload/api/vendor/league/uri-interfaces/IPv6/Converter.php +++ b/upload/api/vendor/league/uri-interfaces/IPv6/Converter.php @@ -171,7 +171,7 @@ public static function normalize(BackedEnum|Stringable|string|null $host): ?stri return strtolower($host); } - $components['ipAddress'] = strtolower($components['ipAddress']); + $components['ipAddress'] = strtolower(self::expandIp($components['ipAddress'])); return self::build($components); } diff --git a/upload/api/vendor/league/uri-interfaces/KeyValuePair/Converter.php b/upload/api/vendor/league/uri-interfaces/KeyValuePair/Converter.php index 39d0b6d8..f8f83855 100644 --- a/upload/api/vendor/league/uri-interfaces/KeyValuePair/Converter.php +++ b/upload/api/vendor/league/uri-interfaces/KeyValuePair/Converter.php @@ -109,7 +109,7 @@ public function encodingMap(): array } /** - * @return array> + * @return list> */ public function toPairs(BackedEnum|Stringable|string|int|float|bool|null $value): array { diff --git a/upload/api/vendor/league/uri-interfaces/Occurrence.php b/upload/api/vendor/league/uri-interfaces/Occurrence.php new file mode 100644 index 00000000..41e7f253 --- /dev/null +++ b/upload/api/vendor/league/uri-interfaces/Occurrence.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri; + +enum Occurrence +{ + case First; + case Last; +} diff --git a/upload/api/vendor/league/uri-interfaces/QueryString.php b/upload/api/vendor/league/uri-interfaces/QueryString.php index ed3d0e04..d952d43a 100644 --- a/upload/api/vendor/league/uri-interfaces/QueryString.php +++ b/upload/api/vendor/league/uri-interfaces/QueryString.php @@ -115,7 +115,7 @@ public static function buildFromPairs(iterable $pairs, ?Converter $converter = n throw new SyntaxError('The pair can not be converted to build a query string.', previous: $typeError); } - $keyValuePairs[] = [(string) Encoder::encodeQueryKeyValue($key), null === $value ? null : Encoder::encodeQueryKeyValue($value)]; + $keyValuePairs[] = [Encoder::encodeQueryKeyValue($key), null === $value ? null : Encoder::encodeQueryKeyValue($value)]; } return ($converter ?? Converter::fromRFC3986())->toValue($keyValuePairs); @@ -375,7 +375,7 @@ public static function parseFromValue(BackedEnum|Stringable|string|bool|null $qu } /** - * @param array> $pairs + * @param list> $pairs * * @return array */ diff --git a/upload/api/vendor/league/uri-interfaces/UriString.php b/upload/api/vendor/league/uri-interfaces/UriString.php index f464fa8c..166c3ec2 100644 --- a/upload/api/vendor/league/uri-interfaces/UriString.php +++ b/upload/api/vendor/league/uri-interfaces/UriString.php @@ -29,6 +29,7 @@ use function filter_var; use function function_exists; use function implode; +use function ltrim; use function preg_match; use function sprintf; use function str_replace; @@ -37,6 +38,7 @@ use function substr; use const FILTER_FLAG_IPV4; +use const FILTER_VALIDATE_INT; use const FILTER_VALIDATE_IP; /** @@ -274,25 +276,58 @@ public static function buildAuthority(array $components): ?string public static function parseNormalized(Stringable|string $uri): array { $components = self::parse($uri); + if (null !== $components['scheme']) { $components['scheme'] = strtolower($components['scheme']); } $components['host'] = self::normalizeHost($components['host']); + $path = $components['path']; $authority = self::buildAuthority($components); - //dot segment only happens when: - // - the path is absolute - // - the scheme and/or the authority are defined - if ('/' === ($path[0] ?? '') || '' !== $components['scheme'].$authority) { - $path = self::removeDotSegments($path); + $absolutePath = str_starts_with($path, '/'); + + // A leading `/.` is used internally to protect a path starting with + // `//` from being interpreted as an authority. + if (null === $authority && str_starts_with($path, '/.//')) { + $path = substr($path, 2); } - // if there is an authority, the path must be absolute - if ('' !== $path && '/' !== $path[0]) { - if (null !== $authority) { - $path = '/'.$path; - } + // Dot-segment removal applies to: + // - absolute paths + // - paths with a scheme or authority + // - relative paths containing dot segments + if ( + '/' === ($path[0] ?? '') + || null !== $components['scheme'] + || null !== $authority + ) { + $path = self::removeDotSegments( + strtr($path, ['%2E' => '.', '%2e' => '.']) + ); + } + + // A path starting with `//` would be interpreted as an authority when + // serialized after a scheme (or as a network-path reference otherwise). + if (null === $authority && str_starts_with($path, '//')) { + $path = ($absolutePath ? '/.' : './').$path; + } + + // If there is an authority, the path must be absolute. + if ('' !== $path && '/' !== $path[0] && null !== $authority) { + $path = '/'.$path; + } + + // A relative path whose first segment contains ':' must be + // prefixed with './' to remain a path-noscheme. + if ( + null === $components['scheme'] + && null === $authority + && '' !== $path + && '/' !== $path[0] + && str_contains(explode('/', $path, 2)[0], ':') + ) { + $path = './'.$path; } $components['path'] = (string) Encoder::normalizePath($path); @@ -359,6 +394,8 @@ public static function resolve(BackedEnum|Stringable|string $uri, BackedEnum|Str $uri = (string) $uri; if ('' === $uri) { $uri = $baseUri ?? throw new SyntaxError("The uri can not be the empty string when there's no base URI."); + // RFC3986 §5.2.2: the target fragment always comes from the reference. + $uri = explode('#', (string) $uri, 2)[0]; } $uriComponents = self::parse($uri); @@ -425,6 +462,12 @@ public static function removeDotSegments(Stringable|string $path): string $newPath .= '/'; } + // A ".." overshooting the root must not turn an absolute path into a + // rootless one; RFC3986 §5.2.4 keeps the leading slash (as BaseUri does). + if (str_starts_with($path, '/') && !str_starts_with($newPath, '/')) { + return '/'.$newPath; + } + return $newPath; } @@ -557,7 +600,7 @@ public static function parse(BackedEnum|Stringable|string|int $uri): array preg_match(self::REGEXP_URI_PARTS, $uri, $parts); $parts += ['query' => '', 'fragment' => '']; - if (':' === ($parts['scheme'] ?? null) || 1 !== preg_match(self::REGEXP_URI_SCHEME, $parts['scontent'] ?? '')) { + if (':' === ($parts['scheme'] ?? null) || 1 !== preg_match(self::REGEXP_URI_SCHEME, $parts['scontent'] ?? '')) { throw new SyntaxError(sprintf('The uri `%s` contains an invalid scheme', $uri)); } @@ -602,10 +645,7 @@ private static function validateComponents(?string $scheme, ?string $authority, return; } - if (str_starts_with($path, '//')) { - throw new SyntaxError('If there is no authority the path `'.$path.'` cannot start with a `//`.'); - } - + !str_starts_with($path, '//') || throw new SyntaxError('If there is no authority the path `'.$path.'` cannot start with a `//`.'); if (null !== $scheme || false === ($pos = strpos($path, ':'))) { return; } @@ -663,11 +703,20 @@ public static function parseAuthority(BackedEnum|Stringable|string|null $authori */ private static function filterPort(string $port): ?int { - return match (true) { - '' === $port => null, - 1 === preg_match('/^\d*$/', $port) => (int) $port, - default => throw new SyntaxError(sprintf('The port `%s` is invalid', $port)), - }; + if ('' === $port) { + return null; + } + + $filteredPort = ltrim($port, '0'); + if ('' === $filteredPort) { + return 0; + } + + $filteredPort = filter_var($filteredPort, FILTER_VALIDATE_INT, ['options' => ['min_range' => 0]]); + + return false !== $filteredPort + ? $filteredPort + : throw new SyntaxError(sprintf('The port `%s` is invalid', $port)); } /** diff --git a/upload/api/vendor/league/uri/TypeConverter.php b/upload/api/vendor/league/uri/TypeConverter.php new file mode 100644 index 00000000..736663cf --- /dev/null +++ b/upload/api/vendor/league/uri/TypeConverter.php @@ -0,0 +1,388 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri; + +use BackedEnum; +use DateTimeImmutable; +use DateTimeInterface; +use DateTimeZone; +use Exception; +use ReflectionEnum; +use Stringable; +use UnitEnum; +use ValueError; + +use function enum_exists; +use function filter_var; +use function is_bool; +use function is_float; +use function is_int; +use function is_iterable; +use function is_scalar; +use function is_string; +use function trim; + +use const FILTER_NULL_ON_FAILURE; +use const FILTER_VALIDATE_BOOLEAN; +use const FILTER_VALIDATE_FLOAT; +use const FILTER_VALIDATE_INT; + +final class TypeConverter +{ + private function __construct() + { + } + + public static function toString(mixed $value): ?string + { + if ($value instanceof BackedEnum) { + $value = $value->value; + } + + return match (true) { + is_bool($value) => true === $value ? '1' : '0', + is_scalar($value), + $value instanceof Stringable => (string) $value, + default => null, + }; + } + + public static function toInteger(mixed $value): ?int + { + if (!is_int($value) && !is_float($value)) { + $value = self::toString($value); + } + + return match (true) { + is_int($value) => $value, + is_string($value) => false !== ($res = filter_var($value, FILTER_VALIDATE_INT)) ? $res : null, + default => null, + }; + } + + public static function toFloat(mixed $value): ?float + { + if (!is_int($value) && !is_float($value)) { + $value = self::toString($value); + } + + return match (true) { + is_float($value) => $value, + is_int($value) => (float) $value, + is_string($value) => false !== ($res = filter_var($value, FILTER_VALIDATE_FLOAT)) ? $res : null, + default => null, + }; + } + + public static function toBoolean(mixed $value): ?bool + { + if (!is_bool($value)) { + $value = self::toString($value); + } + + return match (true) { + is_bool($value) => $value, + is_string($value) => filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE), + default => null, + }; + } + + /** + * @return array + */ + public static function toStrings(mixed $values, ?string $default = null): array + { + if (!is_iterable($values)) { + return []; + } + + $arr = []; + foreach ($values as $index => $value) { + $item = self::toString($value) ?? $default; + if (null === $item) { + return []; + } + + $arr[$index] = $item; + } + + return $arr; + } + + /** + * @return array + */ + public static function toIntegers(mixed $values, ?int $default = null): array + { + if (!is_iterable($values)) { + return []; + } + + $arr = []; + foreach ($values as $index => $value) { + $item = self::toInteger($value) ?? $default; + if (null === $item) { + return []; + } + + $arr[$index] = $item; + } + + return $arr; + } + + /** + * @return array + */ + public static function toFloats(mixed $values, ?float $default = null): array + { + if (!is_iterable($values)) { + return []; + } + + $arr = []; + foreach ($values as $index => $value) { + $item = self::toFloat($value) ?? $default; + if (null === $item) { + return []; + } + + $arr[$index] = $item; + } + + return $arr; + } + + /** + * @return array + */ + public static function toBooleans(mixed $values, ?bool $default = null): array + { + if (!is_iterable($values)) { + return []; + } + + $arr = []; + foreach ($values as $index => $value) { + $item = self::toBoolean($value) ?? $default; + if (null === $item) { + return []; + } + + $arr[$index] = $item; + } + + return $arr; + } + + /** + * @param class-string $enumClass + */ + public static function toEnum(mixed $value, string $enumClass): ?UnitEnum + { + enum_exists($enumClass) || throw new ValueError($enumClass.' does not exist or could not be found.'); + /** @var array $cases */ + $cases = $enumClass::cases(); + + return (new ReflectionEnum($enumClass))->isBacked() + ? self::findBackedEnum($value, $enumClass, $cases) /* @phpstan-ignore-line */ + : self::findEnum($value, $enumClass, $cases); + } + + /** + * @template T of UnitEnum + * + * @param class-string $enumClass + * @param ?T $default + */ + public static function toEnums(mixed $values, string $enumClass, ?UnitEnum $default = null): array + { + if (!is_iterable($values)) { + return []; + } + + enum_exists($enumClass) || throw new ValueError($enumClass.' does not exist or could not be found.'); + null === $default || $default instanceof $enumClass || throw new ValueError('The default value must be an instance of '.$enumClass.'; '.get_debug_type($default).' given.'); + + $arr = []; + $cases = $enumClass::cases(); + /** @var ReflectionEnum $reflection */ + $reflection = new ReflectionEnum($enumClass); + + $createItem = $reflection->isBacked() + ? fn ($item) => self::findBackedEnum($item, $enumClass, $cases) /* @phpstan-ignore-line */ + : fn ($item) => self::findEnum($item, $enumClass, $cases); + + foreach ($values as $index => $value) { + $item = $createItem($value) ?? $default; + if (null === $item) { + return []; + } + + $arr[$index] = $item; + } + + return $arr; + } + + /** + * @template T of BackedEnum + * + * @param class-string $enumClass + * @param array $cases + * + * @return ?T + */ + private static function findBackedEnum(mixed $value, string $enumClass, array $cases): ?UnitEnum + { + if ($value instanceof $enumClass) { + return $value; + } + + if ($value instanceof UnitEnum || is_float($value) || is_bool($value)) { + return null; + } + + if (!is_string($value) && !is_int($value)) { + $value = self::toString($value); + } + + if (null === $value) { + return null; + } + + $intValue = is_int($value) ? $value : self::toInteger($value); + $value = (string) $value; + foreach ($cases as $case) { + if ($intValue === $case->value || $value === $case->value) { + return $case; + } + } + + return null; + } + + /** + * @template T of UnitEnum + * + * @param class-string $enumClass + * @param array $cases + * + * @return ?T + */ + private static function findEnum(mixed $value, string $enumClass, array $cases): ?UnitEnum + { + if ($value instanceof $enumClass) { + return $value; + } + + if ($value instanceof UnitEnum || (is_scalar($value) && !is_string($value))) { + return null; + } + + $value = self::toString($value); + if (null === $value) { + return null; + } + + foreach ($cases as $case) { + if ($case->name === $value) { + return $case; + } + } + + return null; + } + + /** + * @param non-empty-string $format + * + * @throws Exception if the provided timezone is invalid + */ + public static function toDateTimeImmutable(mixed $value, string $format, DateTimeZone|string $timezone = 'UTC'): ?DateTimeImmutable + { + $format = trim($format); + '' !== $format || throw new ValueError('The date format must be a non-empty string.'); + $timezone = !$timezone instanceof DateTimeZone ? new DateTimeZone($timezone) : $timezone; + + return self::createDateTimeImmutable($value, $format, $timezone); + } + + /** + * @param non-empty-string $format + * + * @throws Exception + * + * @return array + */ + public static function toDateTimeImmutables( + mixed $values, + string $format, + DateTimeZone|string $timezone = 'UTC', + DateTimeInterface|null $default = null, + ): array { + if (!is_iterable($values)) { + return []; + } + + $format = trim($format); + '' !== $format || throw new ValueError('The date format must be a non-empty string.'); + $timezone = !$timezone instanceof DateTimeZone ? new DateTimeZone($timezone) : $timezone; + + if (null !== $default && !$default instanceof DateTimeImmutable) { + $default = DateTimeImmutable::createFromInterface($default); + } + + $arr = []; + foreach ($values as $index => $value) { + $item = self::createDateTimeImmutable($value, $format, $timezone) ?? $default; + if (null === $item) { + return []; + } + + $arr[$index] = $item; + } + + return $arr; + } + + /** + * @param non-empty-string $format + */ + private static function createDateTimeImmutable(mixed $value, string $format, DateTimeZone $timezone): ?DateTimeImmutable + { + if ($value instanceof DateTimeImmutable) { + return $value; + } + + if ($value instanceof DateTimeInterface) { + return DateTimeImmutable::createFromInterface($value); + } + + if (is_scalar($value) && !is_string($value)) { + return null; + } + + $value = self::toString($value); + if (!is_string($value) || str_contains($value, "\0")) { + return null; + } + + $date = DateTimeImmutable::createFromFormat($format, $value, $timezone); + $errors = DateTimeImmutable::getLastErrors(); + + return false !== $date + && (false === $errors || (0 === $errors['error_count'] && 0 === $errors['warning_count'])) ? $date : null; + } +} diff --git a/upload/api/vendor/league/uri/Uri.php b/upload/api/vendor/league/uri/Uri.php index 2eaf2e2d..920db9cc 100644 --- a/upload/api/vendor/league/uri/Uri.php +++ b/upload/api/vendor/league/uri/Uri.php @@ -14,7 +14,6 @@ namespace League\Uri; use BackedEnum; -use Closure; use Deprecated; use finfo; use League\Uri\Contracts\Conditionable; @@ -428,7 +427,9 @@ public static function fromFileContents(mixed $path, $context = null): self $finfo = new finfo(FILEINFO_MIME_TYPE); $bufferSize = 8192; - /** @var Closure(SplFileobject): array{0:string, 1:string} $fromFileObject */ + /** + * @return array{0: string, 1: string} + */ $fromFileObject = function (SplFileObject $path) use ($finfo, $bufferSize): array { $raw = $path->fread($bufferSize); false !== $raw || throw new SyntaxError('The file `'.$path.'` does not exist or is not readable.'); @@ -441,7 +442,10 @@ public static function fromFileContents(mixed $path, $context = null): self return [$mimetype, $raw]; }; - /** @var Closure(resource): array{0:string, 1:string} $fromResource */ + /** + * @param resource $stream + * @return array{0: string, 1: string} + */ $fromResource = function ($stream) use ($finfo, $path, $bufferSize): array { set_error_handler(fn (int $errno, string $errstr, string $errfile, int $errline) => true); $raw = fread($stream, $bufferSize); @@ -456,7 +460,10 @@ public static function fromFileContents(mixed $path, $context = null): self return [$mimetype, $raw]; }; - /** @var Closure(Stringable|string, resource|null): array{0:string, 1:string} $fromPath */ + /** + * @param resource|null $context + * @return array{0: string, 1: string} + */ $fromPath = function (Stringable|string $path, $context) use ($finfo): array { $path = (string) $path; set_error_handler(fn (int $errno, string $errstr, string $errfile, int $errline) => true); @@ -693,7 +700,7 @@ private static function fetchRequestUri(array $server): array { $server += ['IIS_WasUrlRewritten' => null, 'UNENCODED_URL' => '', 'PHP_SELF' => '', 'QUERY_STRING' => null]; if ('1' === $server['IIS_WasUrlRewritten'] && '' !== $server['UNENCODED_URL']) { - return explode('?', $server['UNENCODED_URL'], 2) + [1 => null]; + return array_values(explode('?', $server['UNENCODED_URL'], 2) + [1 => null]); } if (isset($server['REQUEST_URI'])) { @@ -753,7 +760,7 @@ private function formatPath(string $path): string */ private static function formatDataPath(string $path): string { - if ('' == $path) { + if ('' === $path) { return 'text/plain;charset=us-ascii,'; } @@ -794,7 +801,10 @@ private static function assertValidPath(string $mimetype, string $parameters, st $parameters = substr($parameters, 0, - strlen($matches[0])); } - $res = array_filter(array_filter(explode(';', $parameters), self::validateParameter(...))); + $res = array_filter( + array_filter(explode(';', $parameters), self::validateParameter(...)), + static fn (string $v): bool => '' !== $v, + ); [] === $res || throw new SyntaxError('The path parameters `'.$parameters.'` is invalid.'); if (!$isBinary) { return; @@ -813,7 +823,7 @@ private static function validateParameter(string $parameter): bool { $properties = explode('=', $parameter); - return 2 != count($properties) || 'base64' === strtolower($properties[0]); + return 2 !== count($properties) || 'base64' === strtolower($properties[0]); } /** @@ -1065,7 +1075,7 @@ private function isUriWithSchemeHostAndPathOnly(): bool && null === $this->port && null === $this->query && null === $this->fragment - && !('' != $this->scheme && null === $this->host); + && !('' !== $this->scheme && null !== $this->scheme && null === $this->host); } /** @@ -1241,7 +1251,7 @@ public function toFileContents(mixed $destination, $context = null): ?int null !== $document || throw new RuntimeException('Unable to extract the document part from the URI path.'); $data = match (true) { - str_ends_with((string) $mediaType, ';base64') => (string) base64_decode($document, true), + str_ends_with($mediaType, ';base64') => (string) base64_decode($document, true), default => rawurldecode($document), }; diff --git a/upload/api/vendor/league/uri/UriTemplate.php b/upload/api/vendor/league/uri/UriTemplate.php index 3f40c212..8f565119 100644 --- a/upload/api/vendor/league/uri/UriTemplate.php +++ b/upload/api/vendor/league/uri/UriTemplate.php @@ -19,9 +19,11 @@ use League\Uri\Contracts\UriInterface; use League\Uri\Exceptions\MissingFeature; use League\Uri\Exceptions\SyntaxError; +use League\Uri\UriTemplate\ExtractionResult; use League\Uri\UriTemplate\Template; use League\Uri\UriTemplate\TemplateCanNotBeExpanded; use League\Uri\UriTemplate\VariableBag; +use League\Uri\UriTemplate\VariableCanNotBeExtracted; use Psr\Http\Message\UriFactoryInterface; use Psr\Http\Message\UriInterface as Psr7UriInterface; use Stringable; @@ -29,6 +31,7 @@ use Uri\Rfc3986\Uri as Rfc3986Uri; use Uri\WhatWg\InvalidUrlException; use Uri\WhatWg\Url as WhatWgUrl; +use Uri\WhatWg\UrlValidationError; use function array_fill_keys; use function array_key_exists; @@ -53,13 +56,13 @@ final class UriTemplate implements Stringable * @throws SyntaxError if the template syntax is invalid * @throws TemplateCanNotBeExpanded if the template or the variables are invalid */ - public function __construct(BackedEnum|Stringable|string $template, iterable $defaultVariables = []) + public function __construct(BackedEnum|Stringable|string $template, iterable|ExtractionResult $defaultVariables = []) { $this->template = $template instanceof Template ? $template : Template::new($template); $this->defaultVariables = $this->filterVariables($defaultVariables); } - private function filterVariables(iterable $variables): VariableBag + private function filterVariables(iterable|ExtractionResult $variables): VariableBag { if (!$variables instanceof VariableBag) { $variables = new VariableBag($variables); @@ -98,6 +101,14 @@ public function getDefaultVariables(): array return iterator_to_array($this->defaultVariables); } + /** + * Tells whether the UriTemplate contains variables. + */ + public function hasVariable(): bool + { + return 0 < count($this->template->variableNames); + } + /** * Returns a new instance with the updated default variables. * @@ -109,7 +120,7 @@ public function getDefaultVariables(): array * * @throws TemplateCanNotBeExpanded if the variables are invalid */ - public function withDefaultVariables(iterable $defaultVariables): self + public function withDefaultVariables(iterable|ExtractionResult $defaultVariables): self { $defaultVariables = $this->filterVariables($defaultVariables); if ($this->defaultVariables->equals($defaultVariables)) { @@ -119,12 +130,12 @@ public function withDefaultVariables(iterable $defaultVariables): self return new self($this->template, $defaultVariables); } - private function templateExpanded(iterable $variables = []): string + private function templateExpanded(iterable|ExtractionResult $variables = []): string { return $this->template->expand($this->filterVariables($variables)->replace($this->defaultVariables)); } - private function templateExpandedOrFail(iterable $variables = []): string + private function templateExpandedOrFail(iterable|ExtractionResult $variables = []): string { return $this->template->expandOrFail($this->filterVariables($variables)->replace($this->defaultVariables)); } @@ -133,7 +144,7 @@ private function templateExpandedOrFail(iterable $variables = []): string * @throws TemplateCanNotBeExpanded if the variables are invalid * @throws UriException if the resulting expansion cannot be converted to a UriInterface instance */ - public function expand(iterable $variables = [], Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string|null $baseUri = null): UriInterface + public function expand(iterable|ExtractionResult $variables = [], Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string|null $baseUri = null): UriInterface { $expanded = $this->templateExpanded($variables); @@ -146,7 +157,7 @@ public function expand(iterable $variables = [], Rfc3986Uri|WhatWgUrl|BackedEnum * @throws InvalidUriException if the base URI cannot be converted to a Uri\Rfc3986\Uri instance * @throws InvalidUriException if the resulting expansion cannot be converted to a Uri\Rfc3986\Uri instance */ - public function expandToUri(iterable $variables = [], Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string|null $baseUri = null): Rfc3986Uri + public function expandToUri(iterable|ExtractionResult $variables = [], Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string|null $baseUri = null): Rfc3986Uri { class_exists(Rfc3986Uri::class) || throw new MissingFeature('Support for '.Rfc3986Uri::class.' requires PHP8.5+ or a polyfill. Run "composer require league/uri-polyfill" or use you own polyfill.'); @@ -154,12 +165,14 @@ class_exists(Rfc3986Uri::class) || throw new MissingFeature('Support for '.Rfc39 } /** - * @throws MissingFeature if no Uri\Whatwg\Url class is found + * @param list|null $errors + * * @throws TemplateCanNotBeExpanded if the variables are invalid * @throws InvalidUrlException if the base URI cannot be converted to a Uri\Whatwg\Url instance * @throws InvalidUrlException if the resulting expansion cannot be converted to a Uri\Whatwg\Url instance + * @throws MissingFeature if no Uri\Whatwg\Url class is found */ - public function expandToUrl(iterable $variables = [], Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string|null $baseUrl = null, array|null &$errors = []): WhatWgUrl + public function expandToUrl(iterable|ExtractionResult $variables = [], Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string|null $baseUrl = null, array|null &$errors = []): WhatWgUrl { class_exists(WhatWgUrl::class) || throw new MissingFeature('Support for '.WhatWgUrl::class.' requires PHP8.5+ or a polyfill. Run "composer require league/uri-polyfill" or use you own polyfill.'); @@ -171,16 +184,16 @@ class_exists(WhatWgUrl::class) || throw new MissingFeature('Support for '.WhatWg * @throws UriException if the resulting expansion cannot be converted to a UriInterface instance */ public function expandToPsr7Uri( - iterable $variables = [], + iterable|ExtractionResult $variables = [], Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string|null $baseUrl = null, UriFactoryInterface $uriFactory = new HttpFactory() ): Psr7UriInterface { - $uriString = $this->templateExpandedOrFail($variables); + $expanded = $this->templateExpanded($variables); return $uriFactory->createUri( null === $baseUrl - ? $uriString - : UriString::resolve($uriString, match (true) { + ? $expanded + : UriString::resolve($expanded, match (true) { $baseUrl instanceof Rfc3986Uri => $baseUrl->toRawString(), $baseUrl instanceof WhatWgUrl => $baseUrl->toUnicodeString(), default => $baseUrl, @@ -192,7 +205,7 @@ public function expandToPsr7Uri( * @throws TemplateCanNotBeExpanded if the variables are invalid or missing * @throws UriException if the resulting expansion cannot be converted to a UriInterface instance */ - public function expandOrFail(iterable $variables = [], Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string|null $baseUri = null): UriInterface + public function expandOrFail(iterable|ExtractionResult $variables = [], Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string|null $baseUri = null): UriInterface { $expanded = $this->templateExpandedOrFail($variables); @@ -205,7 +218,7 @@ public function expandOrFail(iterable $variables = [], Rfc3986Uri|WhatWgUrl|Back * @throws InvalidUriException if the base URI cannot be converted to a Uri\Rfc3986\Uri instance * @throws InvalidUriException if the resulting expansion cannot be converted to a Uri\Rfc3986\Uri instance */ - public function expandToUriOrFail(iterable $variables = [], Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string|null $baseUri = null): Rfc3986Uri + public function expandToUriOrFail(iterable|ExtractionResult $variables = [], Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string|null $baseUri = null): Rfc3986Uri { class_exists(Rfc3986Uri::class) || throw new MissingFeature('Support for '.Rfc3986Uri::class.' requires PHP8.5+ or a polyfill. Run "composer require league/uri-polyfill" or use you own polyfill.'); @@ -213,12 +226,14 @@ class_exists(Rfc3986Uri::class) || throw new MissingFeature('Support for '.Rfc39 } /** + * @param list|null $errors + * * @throws MissingFeature if no Uri\Whatwg\Url class is found * @throws TemplateCanNotBeExpanded if the variables are invalid * @throws InvalidUrlException if the base URI cannot be converted to a Uri\Whatwg\Url instance * @throws InvalidUrlException if the resulting expansion cannot be converted to a Uri\Whatwg\Url instance */ - public function expandToUrlOrFail(iterable $variables = [], Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string|null $baseUrl = null, array|null &$errors = []): WhatWgUrl + public function expandToUrlOrFail(iterable|ExtractionResult $variables = [], Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string|null $baseUrl = null, array|null &$errors = []): WhatWgUrl { class_exists(WhatWgUrl::class) || throw new MissingFeature('Support for '.WhatWgUrl::class.' requires PHP8.5+ or a polyfill. Run "composer require league/uri-polyfill" or use you own polyfill.'); @@ -230,16 +245,16 @@ class_exists(WhatWgUrl::class) || throw new MissingFeature('Support for '.WhatWg * @throws UriException if the resulting expansion cannot be converted to a UriInterface instance */ public function expandToPsr7UriOrFail( - iterable $variables = [], + iterable|ExtractionResult $variables = [], Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string|null $baseUrl = null, UriFactoryInterface $uriFactory = new HttpFactory() ): Psr7UriInterface { - $uriString = $this->templateExpandedOrFail($variables); + $expanded = $this->templateExpandedOrFail($variables); return $uriFactory->createUri( null === $baseUrl - ? $uriString - : UriString::resolve($uriString, match (true) { + ? $expanded + : UriString::resolve($expanded, match (true) { $baseUrl instanceof Rfc3986Uri => $baseUrl->toRawString(), $baseUrl instanceof WhatWgUrl => $baseUrl->toUnicodeString(), default => $baseUrl, @@ -275,6 +290,30 @@ private function newRfc3986Uri(Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string }; } + /** + * Extracts the variables from a URI using the template. + */ + public function extract(Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string $uri): ExtractionResult + { + return $this->template->extract($uri); + } + + /** + * @throws VariableCanNotBeExtracted if the URI cannot be extracted using the template + */ + public function extractOrFail(Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string $uri): ExtractionResult + { + return $this->template->extractOrFail($uri); + } + + /** + * Returns whether the URI matches the template. + */ + public function match(Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string $uri): bool + { + return $this->template->match($uri); + } + /** * DEPRECATION WARNING! This method will be removed in the next major point release. * diff --git a/upload/api/vendor/league/uri/UriTemplate/Expression.php b/upload/api/vendor/league/uri/UriTemplate/Expression.php index d9d5b905..ce258d39 100644 --- a/upload/api/vendor/league/uri/UriTemplate/Expression.php +++ b/upload/api/vendor/league/uri/UriTemplate/Expression.php @@ -14,20 +14,32 @@ namespace League\Uri\UriTemplate; use Deprecated; +use Iterator; +use IteratorAggregate; use League\Uri\Exceptions\SyntaxError; use Stringable; +use ValueError; use function array_filter; use function array_map; +use function array_reverse; use function array_unique; +use function array_values; +use function count; use function explode; use function implode; +use function is_string; +use function ksort; +use function strlen; +use function strpos; +use function substr; /** * @internal The class exposes the internal representation of an Expression and its usage * @link https://www.rfc-editor.org/rfc/rfc6570#section-2.2 + * @implements IteratorAggregate */ -final class Expression +final class Expression implements IteratorAggregate { /** @var array */ private readonly array $varSpecifiers; @@ -64,18 +76,11 @@ public static function new(Stringable|string $expression): self } /** - * DEPRECATION WARNING! This method will be removed in the next major point release. - * - * @throws SyntaxError if the expression is invalid - * @see Expression::new() - * - * @deprecated Since version 7.0.0 - * @codeCoverageIgnore + * @return Iterator */ - #[Deprecated(message:'use League\Uri\UriTemplate\Exppression::new() instead', since:'league/uri:7.0.0')] - public static function createFromString(Stringable|string $expression): self + public function getIterator(): Iterator { - return self::new($expression); + yield from $this->varSpecifiers; } public function expand(VariableBag $variables): string @@ -96,4 +101,269 @@ public function expand(VariableBag $variables): string default => $this->operator->first().$expanded, }; } + + /** + * Extracts variables from a value according to this expression. + * + * Named expressions match variables by name, query expressions match query + * parameters by name regardless of their order, while positional expressions + * assign values according to the order of their variable specifiers. + * + * @throws VariableCanNotBeExtracted If the value cannot be extracted according + * to the expression. + */ + public function extract(string $value): ExtractionResult + { + return $this->operator->isNamed() + ? $this->extractNamedValues($value) + : $this->extractPositionalValues($value); + } + + /** + * Extracts variables from a named expression value. + * + * Named variables are matched by name rather than by position. Non-exploded + * variables may consume at most one component, while an exploded variable + * may consume multiple components. + * + * @throws VariableCanNotBeExtracted If the value cannot be matched to the + * variable specifiers. + */ + private function extractNamedValues(string $value): ExtractionResult + { + /** @var non-empty-string $separator */ + $separator = $this->operator->separator(); + $components = '' === $value ? [] : explode($separator, $value); + $matched = $this->matchNamedValues($components); + null !== $matched || throw VariableCanNotBeExtracted::dueTo('The value "'.$value.'" cannot be extracted from the expression "'.$this->value.'".', ExtractionErrorReason::MalformedValue); + + $variables = []; + foreach ($this->varSpecifiers as $offset => $varSpecifier) { + $extracted = $this->operator->extract($varSpecifier, $matched[$offset] ?? null); + $this->assertPrefixLength($varSpecifier, $extracted); + + foreach ($extracted->names() as $name) { + $variables[$name] = $extracted->fetch($name); + } + } + + return ExtractionResult::success($variables); + } + + /** + * Matches named components against the variable specifiers. + * + * Components are matched by variable name rather than by position. + * Non-exploded variables may occur at most once, while exploded variables + * may consume multiple components. + * + * @param list $components + * + * @return list|null + */ + private function matchNamedValues(array $components): ?array + { + /** @var array> $componentsByName */ + $componentsByName = []; + + foreach ($components as $component) { + [$name] = explode('=', $component, 2); + $componentsByName[$name] ??= []; + $componentsByName[$name][] = $component; + } + + $explodedSpecifiers = []; + foreach ($this->varSpecifiers as $offset => $varSpecifier) { + if ('*' === $varSpecifier->modifier) { + $explodedSpecifiers[$offset] = $varSpecifier; + } + } + + $matched = []; + foreach ($this->varSpecifiers as $offset => $varSpecifier) { + $name = $varSpecifier->name; + if (!isset($componentsByName[$name])) { + $matched[$offset] = null; + + continue; + } + + $occurrences = $componentsByName[$name]; + if ('*' !== $varSpecifier->modifier && 1 !== count($occurrences)) { + return null; + } + + $matched[$offset] = implode($this->operator->separator(), $occurrences); + + unset($componentsByName[$name]); + } + + if ([] !== $componentsByName) { + /* + * Remaining components have names that are not explicitly declared. + * They can only be consumed by a single exploded variable. + */ + if (1 !== count($explodedSpecifiers)) { + return null; + } + + $explodedOffset = array_key_first($explodedSpecifiers); + $remaining = []; + foreach ($componentsByName as $occurrences) { + foreach ($occurrences as $component) { + $remaining[] = $component; + } + } + + $matched[$explodedOffset] = implode($this->operator->separator(), $remaining); + } + + ksort($matched); + + return array_values($matched); + } + + /** + * Extracts variables from a positional expression value. + * + * Values are assigned to variable specifiers in their declared order. + * Exploded variables consume the remaining values required to leave enough + * values for the following variable specifiers. + * + * @throws VariableCanNotBeExtracted If a value cannot be extracted or a value + * exceeds its variable's prefix length. + */ + private function extractPositionalValues(string $value): ExtractionResult + { + return $this->extractPositionalCandidates( + value: $value, + varSpecifierOffset: 0, + valueOffset: 0, + previousResult: ExtractionResult::empty(), + ); + } + + private function extractPositionalCandidates( + string $value, + int $varSpecifierOffset, + int $valueOffset, + ExtractionResult $previousResult, + ): ExtractionResult { + $varSpecifier = $this->varSpecifiers[$varSpecifierOffset]; + $lastVarSpecifier = $varSpecifierOffset + 1 === count($this->varSpecifiers); + if ($lastVarSpecifier) { + $extracted = $this->operator->extract( + $varSpecifier, + substr($value, $valueOffset), + ); + $this->assertPrefixLength($varSpecifier, $extracted); + + return $previousResult->reconcile($extracted); + } + + /** @var non-empty-string $separator */ + $separator = $this->operator->separator(); + $separatorLength = strlen($separator); + $positions = $this->delimiterPositions($value, $valueOffset, $separator); + + $candidates = []; + foreach ($positions as $position) { + $candidates[] = [ + 'end' => $position, + 'next' => $position + $separatorLength, + ]; + } + + if ('*' === $varSpecifier->modifier) { + $candidates = array_reverse($candidates); + } + + $reasons = []; + $missingNames = []; + + foreach ($candidates as ['end' => $end, 'next' => $next]) { + try { + $extracted = $this->operator->extract( + $varSpecifier, + substr($value, $valueOffset, $end - $valueOffset), + ); + $this->assertPrefixLength($varSpecifier, $extracted); + + return $this->extractPositionalCandidates( + value: $value, + varSpecifierOffset: $varSpecifierOffset + 1, + valueOffset: $next, + previousResult: $previousResult->reconcile($extracted), + ); + } catch (VariableCanNotBeExtracted $exception) { + $reasons = [...$reasons, ...$exception->reasons]; + $missingNames = [...$missingNames, ...$exception->missingNames]; + } + } + + throw VariableCanNotBeExtracted::dueToSuitableCandidateNotFound( + $value, + $reasons, + $missingNames, + ); + } + + /** + * Finds all positions of a delimiter at or after the given offset. + * + * @param int $offset The position from which to search. + * + * + * @throws ValueError If the delimiter is empty. + * @return list The positions at which the delimiter occurs. + */ + private function delimiterPositions(string $value, int $offset, string $delimiter): array + { + '' !== $delimiter || throw new ValueError('The delimiter cannot be empty.'); + + $positions = []; + $position = $offset; + + while (false !== ($position = strpos($value, $delimiter, $position))) { + $positions[] = $position; + $position += strlen($delimiter); + } + + return $positions; + } + + /** + * Ensures that an extracted value does not exceed its variable's prefix length. + * + * @throws VariableCanNotBeExtracted If the extracted value exceeds the + * variable's prefix length. + */ + private function assertPrefixLength(VarSpecifier $varSpecifier, ExtractionResult $variables): void + { + if (0 === $varSpecifier->position) { + return; + } + + $value = $variables->fetch($varSpecifier->name); + if (null === $value || !is_string($value->value)) { + return; + } + + (mb_strlen($value->value) <= $varSpecifier->position) || throw VariableCanNotBeExtracted::dueTo('The value for variable "'.$varSpecifier->name.'" exceeds the prefix length.', ExtractionErrorReason::PrefixLengthExceeded); + } + + /** + * DEPRECATION WARNING! This method will be removed in the next major point release. + * + * @throws SyntaxError if the expression is invalid + * @see Expression::new() + * + * @deprecated Since version 7.0.0 + * @codeCoverageIgnore + */ + #[Deprecated(message:'use League\Uri\UriTemplate\Exppression::new() instead', since:'league/uri:7.0.0')] + public static function createFromString(Stringable|string $expression): self + { + return self::new($expression); + } } diff --git a/upload/api/vendor/league/uri/UriTemplate/ExtractedValue.php b/upload/api/vendor/league/uri/UriTemplate/ExtractedValue.php new file mode 100644 index 00000000..9ce7385c --- /dev/null +++ b/upload/api/vendor/league/uri/UriTemplate/ExtractedValue.php @@ -0,0 +1,277 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\UriTemplate; + +use function array_chunk; +use function array_column; +use function array_key_exists; +use function array_map; +use function array_unique; +use function count; +use function explode; +use function in_array; +use function is_array; +use function is_string; + +final class ExtractedValue +{ + public readonly bool $isPartial; + + /** + * @throws VariableCanNotBeExtracted + */ + private function __construct( + public readonly array|string|null $value, + private readonly int $maxLength = -1, + public readonly array $asList = [], + ) { + if (is_array($value)) { + foreach ($value as $key => $item) { + is_string($item) || throw VariableCanNotBeExtracted::dueTo('The extracted value contains a list with an invalid value at key "'.$key.'".', ExtractionErrorReason::TypeMismatch); + } + } + - 1 === $maxLength || (is_string($value) && 0 < $maxLength) || throw VariableCanNotBeExtracted::dueTo('A prefix position can only be associated with a string value.', ExtractionErrorReason::UnsupportedOperation); + + $this->isPartial = -1 !== $maxLength; + } + + public static function fromNull(): self + { + return new self(null); + } + + public static function fromString(string $value, int $maxLength = -1): self + { + return new self($value, $maxLength); + } + + public static function fromArray(array $value): self + { + return new self($value); + } + + /** + * @throws VariableCanNotBeExtracted + */ + public static function fromList(string $value, Operator $operator, VarSpecifier $varSpecifier): self + { + return $operator->isNamed() + ? self::fromNamedList($value, $operator, $varSpecifier) + : self::fromUnnamedList($value, $operator); + } + + /** + * Extracts an exploded variable from a positional representation. + * + * Positional exploded values may contain either plain values or name/value + * pairs, but not both representations at the same time. + */ + public static function fromUnnamedList(string $value, Operator $operator): ExtractedValue + { + return match ($operator) { + Operator::ReservedChars, + Operator::Fragment => self::fromPositionalPairs($value, $operator), + default => self::fromGenericList($value, $operator), + }; + } + + /** + * Creates an extracted value from a path parameter containing alternating + * key/value pairs. + * + * The value is split into comma-separated key/value pairs before decoding, + * so percent-encoded commas (`%2C`) remain part of the corresponding key or value. + * + * @param non-empty-string $value + * + * @throws VariableCanNotBeExtracted if the value is the empty string + */ + public static function fromNamedValue(string $value, Operator $operator): self + { + '' !== $value || throw VariableCanNotBeExtracted::dueTo('The named value is invalid.', ExtractionErrorReason::MalformedValue); + + // Path parameters can encode an associative array as alternating + // key/value pairs. Split before decoding so that encoded commas + // (%2C) remain part of the value. + $parts = explode(',', $value); + $parts = 0 === count($parts) % 2 ? $parts : [...$parts, '']; + $result = []; + foreach (array_chunk($parts, 2) as [$k, $v]) { + $key = $operator->decode($k); + !array_key_exists($key, $result) || throw VariableCanNotBeExtracted::dueTo('The variable cannot be extracted because the input contains multiple values for "'.$key.'".', ExtractionErrorReason::ReconciliationFailed); + + $result[$key] = $operator->decode($v); + } + + return ExtractedValue::fromArray($result); + } + + /** + * Extracts an exploded variable from a positional representation. + * + * Positional exploded values may contain either plain values or name/value + * pairs, but not both representations at the same time. + */ + private static function fromGenericList(string $value, Operator $operator): ExtractedValue + { + /** @var non-empty-string $separator */ + $separator = $operator->separator(); + $values = explode($separator, $value); + $hasPairs = false; + $result = []; + + foreach ($values as $pValue) { + if (str_contains($pValue, '=')) { + $hasPairs = true; + [$key, $qValue] = explode('=', $pValue, 2); + $result[$operator->decode($key)] = $operator->decode($qValue); + continue; + } + + !$hasPairs || throw VariableCanNotBeExtracted::dueTo('The value "'.$value.'" is malformed.', ExtractionErrorReason::MalformedValue); + $result[] = $operator->decode($pValue); + } + + if (!$hasPairs && 1 === count($result)) { + $result = $result[0]; + } + + return new self($result); + } + + private static function fromPositionalPairs(string $value, Operator $operator): self + { + /** @var non-empty-string $separator */ + $separator = $operator->separator(); + $parts = explode($separator, $value); + $result = []; + for ($i = 0, $count = count($parts); $i < $count;) { + $key = $parts[$i++]; + $i < $count || throw VariableCanNotBeExtracted::dueTo('The value "'.$value.'" is malformed.', ExtractionErrorReason::MalformedValue); + $part = $parts[$i++]; + if ('' === $part) { + ($i < $count && '' === $parts[$i]) || throw VariableCanNotBeExtracted::dueTo('The value "'.$value.'" is malformed.', ExtractionErrorReason::MalformedValue); + ++$i; + $part = $separator; + } + + $result[$operator->decode($key)] = $operator->decode($part); + } + + return new self($result); + } + + /** + * Extracts an exploded variable from a named representation. + * + * The value may consist of repeated occurrences of the variable name or of + * name/value pairs. When all pairs use the variable's name, the values are + * returned as a list. Otherwise, the complete name/value mapping is returned, + * provided the variable's name is not mixed with other names. + */ + public static function fromNamedList(string $value, Operator $operator, VarSpecifier $varSpecifier): self + { + /** @var non-empty-string $separator */ + $separator = $operator->separator(); + $items = explode($separator, $value); + $pairs = []; + foreach ($items as $item) { + $pairs[] = str_contains($item, '=') ? explode('=', $item, 2) : [$varSpecifier->name, $item]; + } + + $names = array_unique(array_column($pairs, 0)); + if (1 === count($names) && $varSpecifier->name === $names[0]) { + return new self(array_map(fn (array $pair): string => $operator->decode($pair[1]), $pairs)); + } + + !in_array($varSpecifier->name, $names, true) || throw VariableCanNotBeExtracted::dueTo('The value "'.$value.'" is malformed.', ExtractionErrorReason::MalformedValue); + + $result = []; + foreach ($pairs as [$pName, $pValue]) { + $key = $operator->decode($pName); + !array_key_exists($key, $result) || throw VariableCanNotBeExtracted::dueTo('The variable cannot be extracted because the input contains multiple values for "'.$key.'".', ExtractionErrorReason::ReconciliationFailed); + $result[$key] = $operator->decode($pValue); + } + + return new self($result); + } + + public static function fromValue(string $value, Operator $operator, VarSpecifier $varSpecifier): self + { + return new self( + value: $operator->decode($value), + maxLength: 0 === $varSpecifier->position ? -1 : $varSpecifier->position, + asList: array_map( + static fn (string $value): string => $operator->decode($value), + '' !== $value && $operator->supportsListValue() ? explode(',', $value) : [] + ), + ); + } + + public function equals(mixed $value): bool + { + return $value instanceof self + && $value->maxLength === $this->maxLength + && $value->value === $this->value + && $value->asList === $this->asList; + } + + public function hasKey(string|int $name): bool + { + return is_array($this->value) && array_key_exists($name, $this->value); + } + + /** + * @throws VariableCanNotBeExtracted + */ + public function reconcile(self $other): self + { + if ($this->equals($other)) { + return 1 < count($this->asList) ? new self($this->asList) : $this; + } + + $thisValue = $this->value; + $otherValue = $other->value; + if ([] !== $this->asList && is_array($otherValue) && $this->asList === $otherValue) { + return $other; + } + + if ([] !== $other->asList && is_array($thisValue) && $other->asList === $thisValue) { + return $this; + } + + (!is_array($thisValue) || !is_array($otherValue)) || throw VariableCanNotBeExtracted::dueTo('The extracted lists contain different data.', ExtractionErrorReason::ListMismatch); + (is_string($thisValue) && is_string($otherValue)) || throw VariableCanNotBeExtracted::dueTo('The extracted values have different types.', ExtractionErrorReason::TypeMismatch); + if ($this->maxLength === $other->maxLength) { + ($thisValue === $otherValue) || throw VariableCanNotBeExtracted::dueTo('The extracted values are different.', ExtractionErrorReason::StringMismatch); + + $value = [] === $this->asList ? $other : $this; + + return 1 < count($value->asList) ? new self($value->asList) : $value; + } + + $result = $other; + $resultValue = $otherValue; + $prefixValue = $thisValue; + if (-1 === $this->maxLength || (-1 !== $other->maxLength && $this->maxLength > $other->maxLength)) { + $result = $this; + $resultValue = $thisValue; + $prefixValue = $otherValue; + } + + return str_starts_with($resultValue, $prefixValue) + ? $result + : throw VariableCanNotBeExtracted::dueTo('The extracted value does not start with the other extracted value.', ExtractionErrorReason::StringMismatch); + } +} diff --git a/upload/api/vendor/league/uri/UriTemplate/ExtractionErrorReason.php b/upload/api/vendor/league/uri/UriTemplate/ExtractionErrorReason.php new file mode 100644 index 00000000..1d824b0d --- /dev/null +++ b/upload/api/vendor/league/uri/UriTemplate/ExtractionErrorReason.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\UriTemplate; + +enum ExtractionErrorReason +{ + // Matching + case LiteralMismatch; + case PrefixMismatch; + case VariableMismatch; + case TypeMismatch; + case StringMismatch; + case ListMismatch; + + // Extraction + case MalformedValue; + case PrefixLengthExceeded; + case UnmatchedContent; + case UndeterminedDelimiter; + case UnsupportedOperation; + + // Reconciliation + case ReconciliationFailed; + case MissingVariables; +} diff --git a/upload/api/vendor/league/uri/UriTemplate/ExtractionResult.php b/upload/api/vendor/league/uri/UriTemplate/ExtractionResult.php new file mode 100644 index 00000000..f1c5a05f --- /dev/null +++ b/upload/api/vendor/league/uri/UriTemplate/ExtractionResult.php @@ -0,0 +1,353 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\UriTemplate; + +use ArrayAccess; +use Countable; +use DateTimeImmutable; +use DateTimeInterface; +use DateTimeZone; +use Exception; +use League\Uri\HostRecord; +use League\Uri\TypeConverter; +use LogicException; +use TypeError; +use UnitEnum; +use ValueError; + +use function array_key_exists; +use function array_map; +use function array_values; +use function count; +use function is_array; +use function is_int; +use function is_string; + +/** + * @implements ArrayAccess> + */ +final class ExtractionResult implements ArrayAccess, Countable +{ + /** + * @param array $variables + * @param list $names + * @param list $missingNames + * @param list $reasons + */ + private function __construct( + private readonly array $variables, + private readonly array $names, + private readonly array $missingNames, + private readonly array $reasons, + ) { + } + + public static function empty(): self + { + return new self( + variables: [], + names: [], + missingNames: [], + reasons: [], + ); + } + + public static function failure(VariableCanNotBeExtracted $exception, Template $template): self + { + return new self( + variables: [], + names: array_values($template->variableNames), + missingNames: $exception->missingNames, + reasons: $exception->reasons, + ); + } + + public static function success(iterable $variables = []): self + { + $vars = []; + $names = []; + $missing = []; + foreach ($variables as $name => $variable) { + is_string($name) || is_int($name) || throw new TypeError('An extraction variable name must be a string.'); + !array_key_exists($name, $vars) || throw new ValueError('The variable "'.$name.'" is already defined.'); + $variable instanceof ExtractedValue || throw new TypeError('An extraction result value must be an '.ExtractedValue::class.'.'); + + $name = (string) $name; + $vars[$name] = $variable; + $names[$name] = $name; + if (null === $variable->value) { + $missing[$name] = $name; + } + } + + return new self( + variables: $vars, + names: array_values($names), + missingNames: array_values($missing), + reasons: [], + ); + } + + /** + * @throws VariableCanNotBeExtracted + */ + public function reconcile(self $other): self + { + $result = $this->variables; + + // A variable declared by one expression must not be declared + // in the other expression unless it is compatible + foreach ($other->variables as $name => $value) { + if (!array_key_exists($name, $result)) { + $result[$name] = $value; + continue; + } + + try { + $result[$name] = $result[$name]->reconcile($value); + } catch (VariableCanNotBeExtracted $exception) { + throw VariableCanNotBeExtracted::dueTo('The extracted values for variable "'.$name.'" could not be reconciled; '.$exception->getMessage(), ...$exception->reasons); + } + } + + // A variable declared by one expression must not be consumed as part of + // an exploded variable from another expression. + foreach ([...$this->missingNames, ...$other->missingNames] as $missingName) { + foreach ($result as $name => $value) { + !$value->hasKey($missingName) || throw VariableCanNotBeExtracted::dueTo('The variable "'.$missingName.'" was extracted as part of "'.$name.'".', ExtractionErrorReason::VariableMismatch); + } + } + + return self::success($result); + } + + /** + * Returns true if the extraction is successful. + */ + public function isSuccessful(): bool + { + return [] === $this->reasons; + } + + /** + * @return list + */ + public function reasons(): array + { + return $this->reasons; + } + + /** + * Returns the list of all variable names. + * + * @return list + */ + public function names(): array + { + return $this->names; + } + + /** + * Returns the list of variable names missing from the extraction. + * + * @return list + */ + public function missingNames(): array + { + return $this->missingNames; + } + + /** + * Tells whether some variables are attached to the result. + */ + public function hasValue(): bool + { + return [] !== $this->variables; + } + + /** + * Returns the number of found variables. + */ + public function count(): int + { + return count($this->variables); + } + + /** + * @return null|string|array + */ + public function offsetGet(mixed $offset): null|string|array + { + return is_string($offset) || is_int($offset) + ? $this->fetch($offset)?->value + : throw new TypeError('offset must be a string.'); + } + + public function offsetExists(mixed $offset): bool + { + return (is_string($offset) || is_int($offset)) + && array_key_exists($offset, $this->variables); + } + + public function offsetUnset(mixed $offset): never + { + throw new LogicException(self::class.' is read-only.'); + } + + public function offsetSet(mixed $offset, mixed $value): never + { + throw new LogicException(self::class.' is read-only.'); + } + + /** + * @return array|string|null> + */ + public function variables(): array + { + return array_map( + static fn (ExtractedValue $val): array|string|null => 1 < count($val->asList) ? $val->asList : $val->value, + $this->variables + ); + } + + public function fetch(string|int $variableName): ?ExtractedValue + { + return $this->variables[$variableName] ?? null; + } + + public function array(int|string $name): array + { + $variable = $this->fetch($name); + + return match (true) { + null === $variable => [], + [] !== $variable->asList => $variable->asList, + is_array($variable->value) => $variable->value, + default => [], + }; + } + + public function string(int|string $name, ?string $default = null): ?string + { + return TypeConverter::toString($this->fetch($name)?->value) ?? $default; + } + + public function host(int|string $name, ?string $default = null): ?string + { + return HostRecord::parse($this->string($name))?->toUnicode() ?? $default; + } + + public function strings(int|string $name, ?string $default = null): array + { + return TypeConverter::toStrings($this->array($name), $default); + } + + public function hosts(int|string $name, ?string $default = null): array + { + return array_map( + static fn (?string $value): ?string => HostRecord::from($value)->toUnicode() ?? $default, + $this->strings($name) + ); + } + + public function integer(int|string $name, ?int $default = null): ?int + { + return TypeConverter::toInteger($this->fetch($name)?->value) ?? $default; + } + + /** + * @return array + */ + public function integers(int|string $name, ?int $default = null): array + { + return TypeConverter::toIntegers($this->array($name), $default); + } + + public function float(int|string $name, ?float $default = null): ?float + { + return TypeConverter::toFloat($this->fetch($name)?->value) ?? $default; + } + + /** + * @return array + */ + public function floats(int|string $name, ?float $default = null): array + { + return TypeConverter::toFloats($this->array($name), $default); + } + + public function boolean(int|string $name, ?bool $default = null): ?bool + { + return TypeConverter::toBoolean($this->fetch($name)?->value) ?? $default; + } + + /** + * @return array + */ + public function booleans(int|string $name, ?bool $default = null): array + { + return TypeConverter::toBooleans($this->array($name), $default); + } + + /** + * @param class-string $enumClass + */ + public function enum(int|string $name, string $enumClass, ?UnitEnum $default = null): ?UnitEnum + { + null === $default || $default instanceof $enumClass || throw new ValueError('The default value must be an instance of '.$enumClass.'; '.get_debug_type($default).' given.'); + + return TypeConverter::toEnum($this->fetch($name)?->value, $enumClass) ?? $default; + } + + /** + * @param class-string $enumClass + * + * @return array + */ + public function enums(int|string $name, string $enumClass, ?UnitEnum $default = null): array + { + return TypeConverter::toEnums($this->array($name), $enumClass, $default); + } + + /** + * @param non-empty-string $format + * + * @throws Exception + */ + public function date( + int|string $name, + string $format, + DateTimeZone|string $timezone = 'UTC', + ?DateTimeInterface $default = null + ): ?DateTimeImmutable { + + return TypeConverter::toDateTimeImmutable($this->fetch($name)?->value, $format, $timezone) ?? ( + null !== $default && !$default instanceof DateTimeImmutable + ? DateTimeImmutable::createFromInterface($default) + : $default + ); + } + + /** + * @param non-empty-string $format + * + * @throws Exception + * + * @return array + */ + public function dates(int|string $name, string $format, DateTimeZone|string $timezone = 'UTC', ?DateTimeInterface $default = null): array + { + return TypeConverter::toDateTimeImmutables($this->array($name), $format, $timezone, $default); + } +} diff --git a/upload/api/vendor/league/uri/UriTemplate/Literal.php b/upload/api/vendor/league/uri/UriTemplate/Literal.php new file mode 100644 index 00000000..c5183405 --- /dev/null +++ b/upload/api/vendor/league/uri/UriTemplate/Literal.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\UriTemplate; + +use function preg_replace_callback; +use function rawurlencode; +use function strlen; + +/** + * @internal The class exposes the internal representation of an Literal string + * @link https://www.rfc-editor.org/rfc/rfc6570#section-3.1 + */ +final class Literal +{ + private const REGEXP_CHARACTERS_TO_ENCODE = '/[^A-Za-z\d\-._~:\/?#\[\]@!$&\'()*+,;=%]+|%(?![A-Fa-f\d]{2})/'; + + public readonly string $encoded; + public readonly int $length; + + public function __construct(public readonly string $raw) + { + $this->encoded = self::encode($raw); + $this->length = strlen($this->encoded); + } + + private static function encode(string $raw): string + { + return (string) preg_replace_callback( + self::REGEXP_CHARACTERS_TO_ENCODE, + static fn (array $matches): string => rawurlencode($matches[0]), + $raw, + ); + } + + public function isPrefixOf(string $value): bool + { + return str_starts_with($value, $this->encoded); + } +} diff --git a/upload/api/vendor/league/uri/UriTemplate/Operator.php b/upload/api/vendor/league/uri/UriTemplate/Operator.php index 0316b80d..a23de086 100644 --- a/upload/api/vendor/league/uri/UriTemplate/Operator.php +++ b/upload/api/vendor/league/uri/UriTemplate/Operator.php @@ -13,16 +13,19 @@ namespace League\Uri\UriTemplate; -use League\Uri\Encoder; use League\Uri\Exceptions\SyntaxError; use Stringable; +use function array_pad; +use function explode; use function implode; use function is_array; +use function mb_substr; use function preg_match; +use function preg_replace_callback; +use function rawurldecode; use function rawurlencode; use function str_contains; -use function substr; /** * Processing behavior according to the expression type operator. @@ -34,6 +37,11 @@ */ enum Operator: string { + private const REGEXP_CHARS_INVALID = '/[\x00-\x1f\x7f]/'; + private const REGEXP_CHARS_NOT_UNRESERVED = '/[^A-Za-z0-9_\-.~]/'; + private const REGEXP_CHARS_ENCODED = ',%[A-Fa-f0-9]{2},'; + private const REGEXP_CHARS_TO_ENCODE = '/[^A-Za-z0-9_\-.~!$&\'()*+,;=%:@\/?]+|%(?![A-Fa-f0-9]{2})/'; + /** * Expression regular expression pattern. * @@ -57,6 +65,17 @@ enum Operator: string case QueryPair = '&'; case Fragment = '#'; + public function nextDelimiter(): ?string + { + return match ($this) { + self::Query, + self::QueryPair, + self::ReservedChars => '#', + self::Fragment => null, + default => '?#', + }; + } + public function first(): string { return match ($this) { @@ -74,6 +93,18 @@ public function separator(): string }; } + public function supportsListValue(): bool + { + return match ($this) { + self::None, + self::Path, + self::PathParam, + self::Query, + self::QueryPair => true, + default => false, + }; + } + public function isNamed(): bool { return match ($this) { @@ -82,14 +113,56 @@ public function isNamed(): bool }; } + public function allowEmpty(): bool + { + return match ($this) { + self::None, self::ReservedChars => false, + default => true, + }; + } + /** * Removes percent encoding on reserved characters (used with + and # modifiers). */ - public function decode(string $var): string + public function encode(string $value): string + { + if ('' === $value) { + return $value; + } + + $encoder = static fn (array $found): string => 1 === preg_match(self::REGEXP_CHARS_NOT_UNRESERVED, rawurldecode($found[0])) + ? rawurlencode($found[0]) + : $found[0]; + + return match ($this) { + self::Fragment, + self::ReservedChars => (string) preg_replace_callback(self::REGEXP_CHARS_TO_ENCODE, $encoder, $value), + default => rawurlencode($value), + }; + } + + /** + * Decodes a URI template value. + * + * @throws SyntaxError if the value to decode contains invalid characters. + */ + public function decode(string $value): string { + if ('' === $value) { + return ''; + } + + 1 !== preg_match(self::REGEXP_CHARS_INVALID, $value) || throw new SyntaxError('The value contains invalid encoded characters; "'.$value.'".'); + + $decoder = static fn (array $matches): string => match ($matches[0]) { + '%20', '%2F' => $matches[0], + default => rawurldecode($matches[0]), + }; + return match ($this) { - Operator::ReservedChars, Operator::Fragment => (string) Encoder::encodeQueryOrFragment($var), - default => rawurlencode($var), + self::Fragment, + self::ReservedChars => (string) preg_replace_callback(self::REGEXP_CHARS_ENCODED, $decoder, $value), + default => rawurldecode($value), }; } @@ -107,7 +180,6 @@ public static function parseExpression(Stringable|string $expression): array throw new SyntaxError('The expression "'.$expression.'" is invalid.'); } - /** @var array{operator:string, variables:string} $parts */ $parts = $parts + ['operator' => '']; if ('' !== $parts['operator'] && str_contains(self::RESERVED_OPERATOR, $parts['operator'])) { throw new SyntaxError('The operator used in the expression "'.$expression.'" is reserved.'); @@ -156,10 +228,10 @@ private function inject(array|string $value, VarSpecifier $varSpec): array } if (':' === $varSpec->modifier) { - $value = substr($value, 0, $varSpec->position); + $value = mb_substr($value, 0, $varSpec->position, 'UTF-8'); } - return [$this->decode($value), $this->isNamed()]; + return [$this->encode($value), $this->isNamed()]; } /** @@ -173,9 +245,7 @@ private function inject(array|string $value, VarSpecifier $varSpec): array */ private function replaceList(array $value, VarSpecifier $varSpec): array { - if (':' === $varSpec->modifier) { - throw TemplateCanNotBeExpanded::dueToUnableToProcessValueListWithPrefix($varSpec->name); - } + ':' !== $varSpec->modifier || throw TemplateCanNotBeExpanded::dueToUnableToProcessValueListWithPrefix($varSpec->name); if ([] === $value) { return ['', false]; @@ -189,7 +259,7 @@ private function replaceList(array $value, VarSpecifier $varSpec): array $key = rawurlencode((string) $key); } - $var = $this->decode($var); + $var = $this->encode($var); if ('*' === $varSpec->modifier) { if (!$isList) { $var = $key.'='.$var; @@ -222,4 +292,49 @@ private function replaceList(array $value, VarSpecifier $varSpec): array return [implode(',', $pairs), $useQuery]; } + + /** + * Extracts a variable from an operator value. + * + * A null value represents an absent variable. Exploded variables are delegated + * to list extraction, while named non-exploded variables must contain the + * expected variable name. Path and fragment operators may represent non-exploded + * values as key/value pairs. Extracted values are percent-decoded before being + * returned. + * + * @throws VariableCanNotBeExtracted If the value cannot be extracted according + * to the variable specifier. + */ + public function extract(VarSpecifier $varSpecifier, string|null $value): ExtractionResult + { + $content = match (true) { + null === $value => ExtractedValue::fromNull(), + '*' === $varSpecifier->modifier => ExtractedValue::fromList($value, $this, $varSpecifier), + $this->isNamed() => $this->extractNamedValue($varSpecifier, $value), + self::Fragment === $this => $this->extractFragmentValue($varSpecifier, $value), + default => ExtractedValue::fromValue($value, $this, $varSpecifier), + }; + + return ExtractionResult::success([$varSpecifier->name => $content]); + } + + /** + * @throws VariableCanNotBeExtracted + */ + private function extractNamedValue(VarSpecifier $varSpecifier, string $value): ExtractedValue + { + [$name, $value] = array_pad(explode('=', $value, 2), 2, ''); + $name === $varSpecifier->name || throw VariableCanNotBeExtracted::dueTo('The named variable is invalid.', ExtractionErrorReason::VariableMismatch); + + return self::PathParam === $this && str_contains($value, ',') + ? ExtractedValue::fromNamedValue($value, $this) + : ExtractedValue::fromValue($value, $this, $varSpecifier); + } + + private function extractFragmentValue(VarSpecifier $varSpecifier, string $value): ExtractedValue + { + return str_contains($value, ',') + ? ExtractedValue::fromList($value, $this, $varSpecifier) + : ExtractedValue::fromValue($value, $this, $varSpecifier); + } } diff --git a/upload/api/vendor/league/uri/UriTemplate/Template.php b/upload/api/vendor/league/uri/UriTemplate/Template.php index 6e0d4594..3b8fe498 100644 --- a/upload/api/vendor/league/uri/UriTemplate/Template.php +++ b/upload/api/vendor/league/uri/UriTemplate/Template.php @@ -17,16 +17,27 @@ use Deprecated; use League\Uri\Exceptions\SyntaxError; use Stringable; +use Uri\Rfc3986\Uri as Rfc3986Uri; +use Uri\WhatWg\Url as WhatWgUrl; +use ValueError; use function array_filter; use function array_map; -use function array_reduce; +use function array_merge; +use function array_reverse; use function array_unique; +use function array_values; +use function count; +use function implode; use function preg_match_all; use function preg_replace; -use function str_replace; +use function str_starts_with; +use function strlen; use function strpbrk; +use function strpos; +use function substr; +use const PREG_OFFSET_CAPTURE; use const PREG_SET_ORDER; /** @@ -39,19 +50,20 @@ final class Template implements Stringable */ private const REGEXP_EXPRESSION_DETECTOR = '/(?\{[^}]*})/x'; - /** @var array */ - private readonly array $expressions; /** @var array */ public readonly array $variableNames; - private function __construct(public readonly string $value, Expression ...$expressions) + /** @var list */ + private readonly array $parts; + + private function __construct(public readonly string $value, Expression|Literal ...$parts) { - $this->expressions = $expressions; + $this->parts = array_values($parts); $this->variableNames = array_unique( array_merge( ...array_map( static fn (Expression $expression): array => $expression->variableNames, - $expressions + array_filter($parts, fn (Expression|Literal $p): bool => $p instanceof Expression) ) ) ); @@ -72,23 +84,42 @@ public static function new(BackedEnum|Stringable|string $template): self $remainder = preg_replace(self::REGEXP_EXPRESSION_DETECTOR, '', $template); false === strpbrk($remainder, '{}') || throw new SyntaxError('The template "'.$template.'" contains invalid expressions.'); - preg_match_all(self::REGEXP_EXPRESSION_DETECTOR, $template, $founds, PREG_SET_ORDER); + preg_match_all(self::REGEXP_EXPRESSION_DETECTOR, $template, $founds, PREG_SET_ORDER | PREG_OFFSET_CAPTURE); - return new self($template, ...array_values( - array_reduce($founds, function (array $carry, array $found): array { - if (!isset($carry[$found['expression']])) { - $carry[$found['expression']] = Expression::new($found['expression']); - } + $parts = []; + $offset = 0; + foreach ($founds as $found) { + $expression = $found['expression'][0]; + $position = $found['expression'][1]; - return $carry; - }, []) - )); + if ($position > $offset) { + $parts[] = new Literal(substr($template, $offset, $position - $offset)); + } + + $parts[] = Expression::new($expression); + $offset = $position + strlen($found[0][0]); + } + + if ($offset < strlen($template)) { + $parts[] = new Literal(substr($template, $offset)); + } + + return new self($template, ...$parts); } + public function __toString(): string + { + return $this->value; + } + + /**----------- + * Expand API + ------------*/ + /** * @throws TemplateCanNotBeExpanded if the variables are invalid */ - public function expand(iterable $variables = []): string + public function expand(iterable|ExtractionResult $variables = []): string { if (!$variables instanceof VariableBag) { $variables = new VariableBag($variables); @@ -100,32 +131,363 @@ public function expand(iterable $variables = []): string /** * @throws TemplateCanNotBeExpanded if the variables are invalid or missing */ - public function expandOrFail(iterable $variables = []): string + public function expandOrFail(iterable|ExtractionResult $variables = []): string { if (!$variables instanceof VariableBag) { $variables = new VariableBag($variables); } - $missing = array_filter($this->variableNames, fn (string $name): bool => !isset($variables[$name])); - if ([] !== $missing) { - throw TemplateCanNotBeExpanded::dueToMissingVariables(...$missing); - } + $missing = array_filter($this->variableNames, static fn (string $name): bool => !isset($variables[$name])); + [] === $missing || throw TemplateCanNotBeExpanded::dueToMissingVariables(...$missing); return $this->expandAll($variables); } private function expandAll(VariableBag $variables): string { - return array_reduce( - $this->expressions, - fn (string $uri, Expression $expr): string => str_replace($expr->value, $expr->expand($variables), $uri), - $this->value + return implode('', array_map( + static fn (Literal|Expression $part): string => $part instanceof Literal ? $part->encoded : $part->expand($variables), + $this->parts, + )); + } + + /**----------- + * Extract API + ------------*/ + + /** + * Tells whether the value matches this URI template. + * + * This method performs a strict extraction and returns `true` only when the + * value can be completely matched and all variables can be extracted. + * + * @return bool true`if the value matches the template, `false` otherwise. + */ + public function match(Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string $value): bool + { + try { + $this->extractOrFail($value); + + return true; + } catch (VariableCanNotBeExtracted) { + + return false; + } + } + + /** + * Extracts the variables from a value. + * + * Extraction is lenient: variables that are missing from the value are + * represented in the result, while extraction failures are represented by + * a failed result containing the reasons for the failure. + * + * @return ExtractionResult The extraction result. + */ + public function extract(Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string $value): ExtractionResult + { + try { + return $this->extractAll(self::uriString($value)); + } catch (VariableCanNotBeExtracted $exception) { + return ExtractionResult::failure($exception, $this); + } + } + + /** + * Extracts the variables from a value. + * + * Unlike {@see extract()}, this method is strict and reports extraction + * failures or missing variables by throwing an exception. + * + * @throws VariableCanNotBeExtracted If the value cannot be completely matched, + * a variable cannot be extracted, or a + * variable defined by the template is missing. + * + * @return ExtractionResult The extracted variables. + */ + public function extractOrFail(Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string $value): ExtractionResult + { + $value = self::uriString($value); + try { + $result = $this->extractAll($value); + } catch (VariableCanNotBeExtracted $exception) { + throw VariableCanNotBeExtracted::dueToExtractionFailure($value, $this, $exception); + } + + return [] === $result->missingNames() + ? $result + : throw VariableCanNotBeExtracted::dueToMissingVariables($value, $this, $result); + } + + private static function uriString(Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string $uri): string + { + return match (true) { + $uri instanceof Rfc3986Uri => $uri->toRawString(), + $uri instanceof WhatWgUrl => $uri->toUnicodeString(), + $uri instanceof BackedEnum => (string) $uri->value, + default => (string) $uri, + }; + } + + /** + * @throws VariableCanNotBeExtracted + */ + private function extractAll(string $value): ExtractionResult + { + return $this->extractParts( + value: $value, + partOffset: 0, + valueOffset: 0, + previousResult: ExtractionResult::success(), ); } - public function __toString(): string + /** + * Extracts variables by matching the remaining template parts against the value. + * + * @param int $partOffset The offset of the next template part to match. + * @param int $valueOffset The offset of the next value character to match. + * @param ExtractionResult $previousResult The result accumulated from the + * previously matched template parts. + * + * @throws VariableCanNotBeExtracted If the value cannot be matched against + * the remaining template parts. + */ + private function extractParts( + string $value, + int $partOffset, + int $valueOffset, + ExtractionResult $previousResult, + ): ExtractionResult { + if ($partOffset === count($this->parts)) { + $valueOffset === strlen($value) || throw VariableCanNotBeExtracted::dueTo('The value contains unmatched content: "'.substr($value, $valueOffset).'".', ExtractionErrorReason::UnmatchedContent); + + return $previousResult; + } + + return $this->parts[$partOffset] instanceof Literal + ? $this->extractLiteral($value, $partOffset, $valueOffset, $previousResult) + : $this->extractExpression($value, $partOffset, $valueOffset, $previousResult); + } + + /** + * Matches the literal against the value and continues with the remaining template parts. + * + * @param int $partOffset The offset of the next template part to match. + * @param int $valueOffset The offset of the next value character to match. + * @param ExtractionResult $previousResult The result accumulated from the + * previously matched template parts. + * + * @throws VariableCanNotBeExtracted If the value cannot be matched against + * the remaining template parts. + */ + private function extractLiteral( + string $value, + int $partOffset, + int $valueOffset, + ExtractionResult $previousResult, + ): ExtractionResult { + /** @var Literal $literal */ + $literal = $this->parts[$partOffset]; + $literal->isPrefixOf(substr($value, $valueOffset)) || throw VariableCanNotBeExtracted::dueTo('The literal "'.$literal->encoded.'" does not match the value at the expected position.', ExtractionErrorReason::LiteralMismatch); + + return $this->extractParts($value, $partOffset + 1, $valueOffset + $literal->length, $previousResult); + } + + /** + * Extracts the variables from the expression against the value and continues with the remaining template parts. + * + * @param int $partOffset The offset of the next template part to match. + * @param int $valueOffset The offset of the next value character to match. + * @param ExtractionResult $previousResult The result accumulated from the + * previously matched template parts. + * + * @throws VariableCanNotBeExtracted If the value cannot be matched against + * the remaining template parts. + */ + private function extractExpression( + string $value, + int $partOffset, + int $valueOffset, + ExtractionResult $previousResult, + ): ExtractionResult { + /** @var Expression $expression */ + $expression = $this->parts[$partOffset]; + $prefix = $expression->operator->first(); + if ($expression->operator->allowEmpty() && '' !== $prefix && !str_starts_with(substr($value, $valueOffset), $prefix)) { + return $this->extractParts($value, $partOffset + 1, $valueOffset, $previousResult->reconcile($expression->extract(''))); + } + + $expressionOffset = $this->expressionPrefix($expression, $value, $valueOffset); + if ($partOffset + 1 === count($this->parts)) { + return $this->extractExpressionRemainder($expression, $value, $expressionOffset, $previousResult); + } + + $nextPart = $this->parts[$partOffset + 1]; + $delimiter = $nextPart instanceof Literal ? $nextPart->encoded : $nextPart->operator->first(); + + return $this->extractExpressionCandidates($expression, $value, $partOffset, $expressionOffset, $delimiter, $previousResult); + } + + /** + * Extracts the last variables from the expression and matches the remaining value + * against the end of the template. + * + * @param int $expressionOffset The offset of the expression value to extract. + * @param ExtractionResult $previousResult The result accumulated from the + * previously matched template parts. + * + * @throws VariableCanNotBeExtracted If the expression cannot be extracted or + * the remaining value cannot be matched. + */ + private function extractExpressionRemainder( + Expression $expression, + string $value, + int $expressionOffset, + ExtractionResult $previousResult, + ): ExtractionResult { + $expressionEnd = $this->expressionEnd($expression, $value, $expressionOffset); + $lastVariables = $expression->extract(substr($value, $expressionOffset, $expressionEnd - $expressionOffset)); + + return $this->extractParts($value, count($this->parts), $expressionEnd, $previousResult->reconcile($lastVariables)); + } + + /** + * Extracts an expression by trying each possible delimiter position and + * continues with the remaining template parts until a complete match is found. + * + * For an exploded expression, candidates are matched from the last delimiter + * position to the first to prefer the longest possible value. + * + * When the next template part is an expression that allows an empty value and + * its delimiter is absent, the end of the input is considered as a candidate + * boundary for the current expression. This allows consecutive expressions + * to be extracted when the following expression is missing. + * + * @param int $partOffset The offset of the expression in the template parts. + * @param int $expressionOffset The offset of the expression value in the input. + * @param string $delimiter The delimiter used to identify candidate expression boundaries. + * @param ExtractionResult $previousResult The result accumulated from the + * previously matched template parts. + * + * @throws VariableCanNotBeExtracted If no suitable candidate can satisfy the + * expression and the remaining template parts. + */ + private function extractExpressionCandidates( + Expression $expression, + string $value, + int $partOffset, + int $expressionOffset, + string $delimiter, + ExtractionResult $previousResult, + ): ExtractionResult { + '' !== $delimiter || throw VariableCanNotBeExtracted::dueTo('Unable to determine the delimiter for the expression "'.$expression->value.'".', ExtractionErrorReason::UndeterminedDelimiter); + $positions = $this->delimiterPositions($value, $expressionOffset, $delimiter); + if ([] === $positions) { + $nextPart = $this->parts[$partOffset + 1] ?? null; + if (!$nextPart instanceof Expression || !$nextPart->operator->allowEmpty()) { + return $this->extractParts($value, $partOffset + 1, $expressionOffset, $previousResult->reconcile($expression->extract(''))); + } + $positions[] = strlen($value); + } + + if ($expression->operator->first() === $delimiter) { + foreach ($expression as $varSpecifier) { + if ('*' === $varSpecifier->modifier) { + $positions = array_reverse($positions); + break; + } + } + } + + $reasons = []; + $missingNames = []; + foreach ($positions as $position) { + try { + $newVar = $expression->extract(substr($value, $expressionOffset, $position - $expressionOffset)); + + return $this->extractParts($value, $partOffset + 1, $position, $previousResult->reconcile($newVar)); + } catch (VariableCanNotBeExtracted $exception) { + $reasons = [...$reasons, ...$exception->reasons]; + $missingNames = [...$missingNames, ...$exception->missingNames]; + } + } + + throw VariableCanNotBeExtracted::dueToSuitableCandidateNotFound($value, $reasons, $missingNames); + } + + /** + * Validates and consumes the operator prefix of an expression. + * + * @param int $valueOffset The offset of the expression value in the input. + * + * + * @throws VariableCanNotBeExtracted If the expression prefix does not match + * the value at the expected position. + * @return int The offset immediately after the expression prefix. + */ + private function expressionPrefix(Expression $expression, string $value, int $valueOffset): int { - return $this->value; + $prefix = $expression->operator->first(); + + return ('' === $prefix || str_starts_with(substr($value, $valueOffset), $prefix)) + ? $valueOffset + strlen($prefix) + : throw VariableCanNotBeExtracted::dueTo('The prefix "'.$prefix.'" does not match the value for the expression "'.$expression->value.'".', ExtractionErrorReason::PrefixMismatch); + } + + /** + * Finds the end of an expression value according to the URI component + * boundaries imposed by its operator. + * + * Query expressions stop at a fragment delimiter, while expressions in other + * components stop at either a query or fragment delimiter. Fragment expressions + * have no following component boundary. + * + * @param int $offset The offset at which the expression value starts. + * + * @return int The offset of the first component delimiter, or the end of the value + * when no delimiter is found. + */ + private function expressionEnd(Expression $expression, string $value, int $offset): int + { + $delimiters = $expression->operator->nextDelimiter(); + if (null === $delimiters) { + return strlen($value); + } + + $length = strlen($value); + + for ($position = $offset; $position < $length; ++$position) { + if (str_contains($delimiters, $value[$position])) { + return $position; + } + } + + return $length; + } + + /** + * Finds all positions of a delimiter at or after the given offset. + * + * @param int $offset The position from which to search. + * + * + * @throws ValueError If the delimiter is empty. + * @return list The positions at which the delimiter occurs. + */ + private function delimiterPositions(string $value, int $offset, string $delimiter): array + { + '' !== $delimiter || throw new ValueError('The delimiter cannot be empty.'); + + $positions = []; + $position = $offset; + + while (false !== ($position = strpos($value, $delimiter, $position))) { + $positions[] = $position; + $position += strlen($delimiter); + } + + return $positions; } /** diff --git a/upload/api/vendor/league/uri/UriTemplate/VarSpecifier.php b/upload/api/vendor/league/uri/UriTemplate/VarSpecifier.php index 9d6773f3..99971440 100644 --- a/upload/api/vendor/league/uri/UriTemplate/VarSpecifier.php +++ b/upload/api/vendor/league/uri/UriTemplate/VarSpecifier.php @@ -28,7 +28,13 @@ final class VarSpecifier * * @link https://tools.ietf.org/html/rfc6570#section-2.3 */ - private const REGEXP_VARSPEC = '/^(?(?:[A-z0-9_\.]|%[0-9a-fA-F]{2})+)(?\:(?\d+)|\*)?$/'; + private const REGEXP_VARSPEC = '/^ + (? + (?:[A-Za-z0-9_\[\]]|%[0-9a-fA-F]{2})+ + (?:\.(?:[A-Za-z0-9_\[\]]|%[0-9a-fA-F]{2})+)* + ) + (?\:(?\d+)|\*)? + $/x'; private const MODIFIER_POSITION_MAX_POSITION = 10_000; @@ -45,7 +51,10 @@ public static function new(string $specification): self $properties = ['name' => $parsed['name'], 'modifier' => $parsed['modifier'] ?? '', 'position' => $parsed['position'] ?? '']; if ('' !== $properties['position']) { + 1 === preg_match('/^(?:0|[1-9]\d*)$/', $properties['position']) || throw new SyntaxError('The variable specification "'.$specification.'" is invalid.'); $properties['position'] = (int) $properties['position']; + 0 !== $properties['position'] || throw new SyntaxError('The variable specification "'.$specification.'" is invalid the position modifier must be greater than 0.'); + $properties['modifier'] = ':'; } @@ -53,9 +62,7 @@ public static function new(string $specification): self $properties['position'] = 0; } - if (self::MODIFIER_POSITION_MAX_POSITION <= $properties['position']) { - throw new SyntaxError('The variable specification "'.$specification.'" is invalid the position modifier must be lower than 10000.'); - } + self::MODIFIER_POSITION_MAX_POSITION > $properties['position'] || throw new SyntaxError('The variable specification "'.$specification.'" is invalid the position modifier must be lower than 10000.'); return new self($properties['name'], $properties['modifier'], $properties['position']); } diff --git a/upload/api/vendor/league/uri/UriTemplate/VariableBag.php b/upload/api/vendor/league/uri/UriTemplate/VariableBag.php index 61290c11..7206065c 100644 --- a/upload/api/vendor/league/uri/UriTemplate/VariableBag.php +++ b/upload/api/vendor/league/uri/UriTemplate/VariableBag.php @@ -46,10 +46,14 @@ final class VariableBag implements ArrayAccess, Countable, IteratorAggregate private array $variables = []; /** - * @param iterable $variables + * @param iterable|ExtractionResult $variables */ - public function __construct(iterable $variables = []) + public function __construct(iterable|ExtractionResult $variables = []) { + if ($variables instanceof ExtractionResult) { + $variables = array_filter($variables->variables(), static fn (array|string|null $value): bool => null !== $value); + } + foreach ($variables as $name => $value) { $this->assign((string) $name, $value); } @@ -80,7 +84,10 @@ public function offsetSet(mixed $offset, mixed $value): void $this->assign($offset, $value); /* @phpstan-ignore-line */ } - public function offsetGet(mixed $offset): mixed + /** + * @return null|string|array + */ + public function offsetGet(mixed $offset): null|string|array { return $this->fetch($offset); } diff --git a/upload/api/vendor/league/uri/UriTemplate/VariableCanNotBeExtracted.php b/upload/api/vendor/league/uri/UriTemplate/VariableCanNotBeExtracted.php new file mode 100644 index 00000000..0fa0cc47 --- /dev/null +++ b/upload/api/vendor/league/uri/UriTemplate/VariableCanNotBeExtracted.php @@ -0,0 +1,113 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\UriTemplate; + +use Exception; +use League\Uri\Contracts\UriException; +use Throwable; +use TypeError; + +use function array_values; +use function get_debug_type; +use function implode; +use function is_string; + +class VariableCanNotBeExtracted extends Exception implements UriException +{ + /** @var list */ + public readonly array $names; + /** @var list */ + public readonly array $missingNames; + /** @var list */ + public readonly array $reasons; + + protected function __construct( + string $message, + array $reasons, + array $names = [], + array $missingNames = [], + ?Throwable $previous = null, + ) { + parent::__construct($message, previous: $previous); + + $this->reasons = self::uniqueReasons($reasons); + $this->missingNames = self::uniqueNames($missingNames, 'Missing variable name'); + $this->names = self::uniqueNames($names, 'Expected variable name'); + } + + /** + * @return list + */ + private static function uniqueReasons(iterable $reasons): array + { + $unique = []; + foreach ($reasons as $reason) { + $reason instanceof ExtractionErrorReason || throw new TypeError('An extraction error value must be an '.ExtractionErrorReason::class.'; '.get_debug_type($reason).' received.'); + $unique[$reason->name] = $reason; + } + + return array_values($unique); + } + + /** + * @return list + */ + private static function uniqueNames(iterable $names, string $message): array + { + $unique = []; + foreach ($names as $name) { + is_string($name) || throw new TypeError($message.' must be a string; '.get_debug_type($name).' received.'); + $unique[$name] = $name; + } + + return array_values($unique); + } + + public static function dueTo(string $message, ExtractionErrorReason ...$reasons): self + { + return new self($message, array_values($reasons)); + } + + public static function dueToMissingVariables(string $input, Template $template, ExtractionResult|iterable $missingNames): self + { + $missingNames = $missingNames instanceof ExtractionResult ? $missingNames->missingNames() : self::uniqueNames($missingNames, 'Missing variable name'); + + return new self( + message: 'The value "'.$input.'" does not provide all the variables defined by the template "'.$template->value.'"; Missing: "'.implode('", "', $missingNames).'".', + reasons: [ExtractionErrorReason::MissingVariables], + names: $template->variableNames, + missingNames: $missingNames, + ); + } + + public static function dueToExtractionFailure(string $input, Template $template, self $previous): self + { + return new self( + message: 'The value "'.$input.'" could not be completely extracted using the template "'.$template->value.'"', + reasons: $previous->reasons, + names: $template->variableNames, + missingNames: $previous->missingNames, + previous: $previous, + ); + } + + public static function dueToSuitableCandidateNotFound(string $input, array $reasons, array $missingNames = []): self + { + return new self( + message: 'No suitable candidate was found to satisfy the complete extraction of "'.$input.'".', + reasons: $reasons, + missingNames: $missingNames, + ); + } +} diff --git a/upload/api/vendor/league/uri/UrlPattern.php b/upload/api/vendor/league/uri/UrlPattern.php new file mode 100644 index 00000000..ec7ba549 --- /dev/null +++ b/upload/api/vendor/league/uri/UrlPattern.php @@ -0,0 +1,219 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri; + +use BackedEnum; +use League\Uri\Contracts\UriInterface; +use League\Uri\Exceptions\SyntaxError; +use League\Uri\UrlPattern\Component; +use League\Uri\UrlPattern\ComponentName; +use League\Uri\UrlPattern\ComponentResult; +use League\Uri\UrlPattern\MatchMode; +use League\Uri\UrlPattern\PatternType; +use League\Uri\UrlPattern\Result; +use Psr\Http\Message\UriInterface as Psr7UriInterface; +use Stringable; +use Throwable; +use Uri\Rfc3986\Uri as Rfc3986Uri; +use Uri\WhatWg\Url as WhatWgUrl; +use ValueError; + +use function array_key_exists; +use function array_map; +use function get_debug_type; + +/** + * @property-read ?string $scheme + * @property-read ?string $username + * @property-read ?string $password + * @property-read ?string $host + * @property-read ?string $port + * @property-read ?string $path + * @property-read ?string $query + * @property-read ?string $fragment + */ +final class UrlPattern +{ + /** @var array, Component> $components */ + private readonly array $components; + public readonly MatchMode $matchMode; + public readonly bool $hasRegexpGroup; + private readonly bool $hasVariable; + public readonly PatternType $type; + + /** + * @param array $patternComponents + */ + public function __construct(array $patternComponents, MatchMode $matchMode = MatchMode::CaseSensitive) + { + $hasRegexpGroup = false; + $hasVariable = false; + $components = []; + foreach (ComponentName::cases() as $name) { + if (!array_key_exists($name->value, $patternComponents)) { + if (ComponentName::Port !== $name) { + $components[$name->value] = Component::fromAsterisk(); + continue; + } + + $components[ComponentName::Port->value] = array_key_exists(ComponentName::Host->value, $patternComponents) + ? Component::fromEmpty() + : Component::fromAsterisk(); + continue; + } + + $component = $patternComponents[$name->value]; + $component instanceof Component || throw new ValueError('the component must be a "'.Component::class.'"; '.get_debug_type($component).' given.'); + $components[$name->value] = $component; + $hasRegexpGroup = $hasRegexpGroup || $component->hasRegexpGroup; + $hasVariable = $hasVariable || $component->hasVariable; + } + + $this->components = $components; + $this->matchMode = $matchMode; + $this->hasRegexpGroup = $hasRegexpGroup; + $this->hasVariable = $hasVariable; + $this->type = '*' !== $this->components[ComponentName::Scheme->value]->pattern ? PatternType::Absolute : PatternType::Relative; + } + + public static function parse( + Stringable|string $pattern, + Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string|null $baseUrl = null, + MatchMode $matchMode = MatchMode::CaseSensitive + ): ?self { + try { + return self::from($pattern, $baseUrl, $matchMode); + } catch (Throwable) { + return null; + } + } + + /** + * @throws SyntaxError + */ + public static function from( + Stringable|string $pattern, + Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string|null $baseUrl = null, + MatchMode $matchMode = MatchMode::CaseSensitive + ): self { + + return UrlPatternBuilder::from($pattern, $baseUrl) + ->build(matchMode: $matchMode, type: PatternType::Absolute); + } + + public function match(Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string $input): bool + { + try { + return $this->extract($input) instanceof Result; + } catch (Throwable) { + return false; + } + } + + /** + * @throws SyntaxError + */ + public function extract(Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string $input): ?Result + { + $components = self::components($input); + $result = []; + foreach ($this->components as $name => $component) { + $found = ComponentResult::extract($components[$name], $component, ComponentName::from($name), $this->matchMode); + if (null === $found) { + return null; + } + + $result[$name] = $found; + } + + return Result::tryFrom($result, $input); + } + + public function hasVariable(): bool + { + return $this->hasVariable; + } + + public function __get(string $name): string + { + $key = ComponentName::tryFrom($name)?->value; + null !== $key || throw new ValueError('the property named "'.$name.'" does not exist.'); + + return $this->components[$key]->pattern; + } + + /** + * @return array, string> + */ + public function __debugInfo(): array + { + return array_map(static fn (Component $component): string => $component->pattern, $this->components); + } + + /** + * @throws SyntaxError + * + * @return array{ + * scheme: string, + * username: string, + * password: string, + * host: string, + * port: string, + * path: string, + * query: string, + * fragment: string + * } + */ + private static function components(WhatWgUrl|Rfc3986Uri|Stringable|BackedEnum|string $input): array + { + if ($input instanceof WhatWgUrl || $input instanceof UriInterface || $input instanceof Rfc3986Uri) { + return [ + ComponentName::Scheme->value => (string) $input->getScheme(), + ComponentName::Username->value => (string) $input->getUsername(), + ComponentName::Password->value => (string) $input->getPassword(), + ComponentName::Host->value => (string) ($input instanceof WhatWgUrl ? $input->getAsciiHost() : $input->getHost()), + ComponentName::Port->value => (string) $input->getPort(), + ComponentName::Path->value => $input->getPath(), + ComponentName::Query->value => (string) $input->getQuery(), + ComponentName::Fragment->value => (string) $input->getFragment(), + ]; + } + + if ($input instanceof Psr7UriInterface) { + $username = ''; + $password = ''; + $userInfo = $input->getUserInfo(); + if ('' !== $userInfo) { + [$username, $password] = explode(':', $userInfo, 2) + [1 => '']; + } + + return [ + ComponentName::Scheme->value => $input->getScheme(), + ComponentName::Username->value => $username, + ComponentName::Password->value => $password, + ComponentName::Host->value => $input->getHost(), + ComponentName::Port->value => (string) $input->getPort(), + ComponentName::Path->value => $input->getPath(), + ComponentName::Query->value => $input->getQuery(), + ComponentName::Fragment->value => $input->getFragment(), + ]; + } + + $components = array_map(static fn (string|int|null $value): string => (string)$value, UriString::parse($input)); + $components[ComponentName::Username->value] = $components['user']; + $components[ComponentName::Password->value] = $components['pass']; + + return $components; + } +} diff --git a/upload/api/vendor/league/uri/UrlPattern/Component.php b/upload/api/vendor/league/uri/UrlPattern/Component.php new file mode 100644 index 00000000..03756210 --- /dev/null +++ b/upload/api/vendor/league/uri/UrlPattern/Component.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\UrlPattern; + +final class Component +{ + public readonly bool $hasRegexpGroup; + public readonly bool $hasVariable; + + /** + * @param list $parts + */ + private function __construct( + public readonly string $pattern, + public readonly array $parts, + public readonly string $regexp, + ) { + $hasRegexpGroup = false; + $hasVariable = false; + foreach ($this->parts as $part) { + $hasRegexpGroup = $hasRegexpGroup || $part->type->hasRegexpGroup(); + $hasVariable = $hasVariable || $part->type->hasVariable(); + } + + $this->hasRegexpGroup = $hasRegexpGroup; + $this->hasVariable = $hasVariable; + } + + public static function fromAsterisk(): self + { + static $instance; + $instance ??= self::fromPattern('*'); + + return $instance; + } + + public static function fromEmpty(): self + { + static $instance; + $instance ??= self::fromPattern(''); + + return $instance; + } + + public static function fromPattern(string $pattern): self + { + return new self($pattern, PathToRegexp::parse($pattern), PathToRegexp::stringToRegexp($pattern)); + } +} diff --git a/upload/api/vendor/league/uri/UrlPattern/ComponentName.php b/upload/api/vendor/league/uri/UrlPattern/ComponentName.php new file mode 100644 index 00000000..00fffa1f --- /dev/null +++ b/upload/api/vendor/league/uri/UrlPattern/ComponentName.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\UrlPattern; + +enum ComponentName: string +{ + case Scheme = 'scheme'; + case Username = 'username'; + case Password = 'password'; + case Host = 'host'; + case Port = 'port'; + case Path = 'path'; + case Query = 'query'; + case Fragment = 'fragment'; +} diff --git a/upload/api/vendor/league/uri/UrlPattern/ComponentResult.php b/upload/api/vendor/league/uri/UrlPattern/ComponentResult.php new file mode 100644 index 00000000..186a7544 --- /dev/null +++ b/upload/api/vendor/league/uri/UrlPattern/ComponentResult.php @@ -0,0 +1,180 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\UrlPattern; + +use ArrayAccess; +use Countable; +use DateTimeImmutable; +use DateTimeInterface; +use DateTimeZone; +use Exception; +use League\Uri\Encoder; +use League\Uri\HostRecord; +use League\Uri\TypeConverter; +use LogicException; +use TypeError; +use UnitEnum; +use ValueError; + +use function array_key_exists; +use function count; +use function is_int; +use function is_string; +use function preg_match; + +/** + * @implements ArrayAccess + */ +final class ComponentResult implements ArrayAccess, Countable +{ + /** + * @param array $groups + */ + private function __construct( + public readonly string $input, + private readonly array $groups + ) { + } + + public static function extract( + string $input, + Component $component, + ComponentName $componentName, + MatchMode $matchMode, + ): ?self { + $modifier = MatchMode::CaseInsensitive === $matchMode ? 'i' : ''; + $regexp = '~'.$component->regexp.'~'.$modifier; + if (1 !== preg_match($regexp, $input, $matches)) { + return null; + } + + $data = []; + $matchIndex = 1; + foreach ($component->parts as $part) { + if (PartType::Fixed === $part->type) { + continue; + } + + $content = $matches[$matchIndex++] ?? null; + $data[$part->name] = ComponentName::Host === $componentName + ? HostRecord::from($content)->toUnicode() + : Encoder::decodeAll($content); + } + + return new self($input, $data); + } + + public static function empty(): self + { + return new self('', ['']); + } + + public function count(): int + { + return count($this->groups); + } + + public function hasValue(): bool + { + return [''] !== $this->groups && [] !== $this->groups; + } + + /** + * @return list + */ + public function names(): array + { + return array_keys($this->groups); + } + + /** + * @return array + */ + public function variables(): array + { + return $this->groups; + } + + public function offsetGet(mixed $offset): null|string + { + return is_string($offset) || is_int($offset) + ? ($this->groups[$offset] ?? null) + : throw new TypeError('invalid offset type, only string or integer are allowed.'); + } + + public function offsetExists(mixed $offset): bool + { + return (is_string($offset) || is_int($offset)) + && array_key_exists($offset, $this->groups); + } + + public function offsetUnset(mixed $offset): never + { + throw new LogicException(self::class.' is read-only.'); + } + + public function offsetSet(mixed $offset, mixed $value): never + { + throw new LogicException(self::class.' is read-only.'); + } + + public function string(int|string $name, ?string $default = null): ?string + { + return TypeConverter::toString($this->groups[$name] ?? null) ?? $default; + } + + public function integer(int|string $name, ?int $default = null): ?int + { + return TypeConverter::toInteger($this->groups[$name] ?? null) ?? $default; + } + + public function float(int|string $name, ?float $default = null): ?float + { + return TypeConverter::toFloat($this->groups[$name] ?? null) ?? $default; + } + + public function boolean(int|string $name, ?bool $default = null): ?bool + { + return TypeConverter::toBoolean($this->groups[$name] ?? null) ?? $default; + } + + /** + * @param class-string $enumClass + */ + public function enum(int|string $name, string $enumClass, ?UnitEnum $default = null): ?UnitEnum + { + null === $default || $default instanceof $enumClass || throw new ValueError('The default value must be an instance of '.$enumClass.'; '.get_debug_type($default).' given.'); + + return TypeConverter::toEnum($this->groups[$name] ?? null, $enumClass) ?? $default; + } + + /** + * @param non-empty-string $format + * + * @throws Exception + */ + public function date( + int|string $name, + string $format, + DateTimeZone|string $timezone = 'UTC', + ?DateTimeInterface $default = null + ): ?DateTimeImmutable { + + return TypeConverter::toDateTimeImmutable($this->groups[$name] ?? null, $format, $timezone) ?? ( + null !== $default && !$default instanceof DateTimeImmutable + ? DateTimeImmutable::createFromInterface($default) + : $default + ); + } +} diff --git a/upload/api/vendor/league/uri/UrlPattern/MatchMode.php b/upload/api/vendor/league/uri/UrlPattern/MatchMode.php new file mode 100644 index 00000000..3c1c76f2 --- /dev/null +++ b/upload/api/vendor/league/uri/UrlPattern/MatchMode.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\UrlPattern; + +enum MatchMode +{ + case CaseSensitive; + case CaseInsensitive; +} diff --git a/upload/api/vendor/league/uri/UrlPattern/Modifier.php b/upload/api/vendor/league/uri/UrlPattern/Modifier.php new file mode 100644 index 00000000..16f6b00c --- /dev/null +++ b/upload/api/vendor/league/uri/UrlPattern/Modifier.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\UrlPattern; + +enum Modifier: string +{ + case ZeroOrMore = '*'; + case Optional = '?'; + case OneOrMore = '+'; + case None = ''; +} diff --git a/upload/api/vendor/league/uri/UrlPattern/Parser.php b/upload/api/vendor/league/uri/UrlPattern/Parser.php new file mode 100644 index 00000000..ca77429d --- /dev/null +++ b/upload/api/vendor/league/uri/UrlPattern/Parser.php @@ -0,0 +1,446 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\UrlPattern; + +use TypeError; + +use function array_key_exists; +use function count; +use function in_array; +use function min; +use function preg_match; +use function strtolower; +use function substr; + +final class Parser +{ + private const SPECIAL_SCHEMES = ['ftp', 'file', 'http', 'https', 'ws', 'wss']; + + /** @var list */ + private array $tokenList = []; + + private int $tokenIndex = 0; + + private int $tokenIncrement = 1; + + private int $componentStart = 0; + + private State $state = State::Init; + + private int $groupDepth = 0; + + private int $hostnameIPv6BracketDepth = 0; + + private bool $shouldTreatAsStandardUrl = false; + + /** @var array */ + private array $components = []; + + public function __construct(private string $input) + { + $this->parse(); + } + + public function components(): array + { + return $this->components; + } + + public function hasRegexpGroup(): bool + { + //@todo + return false; + } + + private function parse(): array + { + $this->tokenList = PathToRegexp::lex($this->input, lenient: true); + + for (; $this->tokenIndex < count($this->tokenList); $this->tokenIndex += $this->tokenIncrement) { + $this->tokenIncrement = 1; + + if (TokenType::End === $this->currentToken()->type) { + if (State::Init === $this->state) { + $this->rewind(); + if ($this->isHashPrefix()) { + $this->changeState(State::Fragment, 1); + } elseif ($this->isSearchPrefix()) { + $this->changeState(State::Query, 1); + } else { + $this->changeState(State::Path, 0); + } + + continue; + } + + if (State::Authority === $this->state) { + $this->rewindAndSetState(State::Host); + + continue; + } + + $this->changeState(State::Done, 0); + + break; + } + + if ($this->groupDepth > 0) { + if ($this->isGroupClose()) { + --$this->groupDepth; + } else { + continue; + } + } + + if ($this->isGroupOpen()) { + ++$this->groupDepth; + + continue; + } + + switch ($this->state) { + case State::Init: + if ($this->isProtocolSuffix()) { + $this->rewindAndSetState(State::Scheme); + } + + break; + + case State::Scheme: + if ($this->isProtocolSuffix()) { + $this->computeShouldTreatAsStandardUrl(); + + $nextState = State::Path; + $skip = 1; + + if ($this->nextIsAuthoritySlashes()) { + $nextState = State::Authority; + $skip = 3; + } elseif ($this->shouldTreatAsStandardUrl) { + $nextState = State::Authority; + } + + $this->changeState($nextState, $skip); + } + + break; + + case State::Authority: + if ($this->isIdentityTerminator()) { + $this->rewindAndSetState(State::Username); + } elseif ( + $this->isPathnameStart() + || $this->isSearchPrefix() + || $this->isHashPrefix() + ) { + $this->rewindAndSetState(State::Host); + } + + break; + + case State::Username: + if ($this->isPasswordPrefix()) { + $this->changeState(State::Password, 1); + } elseif ($this->isIdentityTerminator()) { + $this->changeState(State::Host, 1); + } + + break; + + case State::Password: + if ($this->isIdentityTerminator()) { + $this->changeState(State::Host, 1); + } + + break; + + case State::Host: + if ($this->isIPv6Open()) { + ++$this->hostnameIPv6BracketDepth; + } elseif ($this->isIPv6Close()) { + --$this->hostnameIPv6BracketDepth; + } + + if ( + $this->isPortPrefix() + && 0 === $this->hostnameIPv6BracketDepth + ) { + $this->changeState(State::Port, 1); + } elseif ($this->isPathnameStart()) { + $this->changeState(State::Path, 0); + } elseif ($this->isSearchPrefix()) { + $this->changeState(State::Query, 1); + } elseif ($this->isHashPrefix()) { + $this->changeState(State::Fragment, 1); + } + + break; + + case State::Port: + if ($this->isPathnameStart()) { + $this->changeState(State::Path, 0); + } elseif ($this->isSearchPrefix()) { + $this->changeState(State::Query, 1); + } elseif ($this->isHashPrefix()) { + $this->changeState(State::Fragment, 1); + } + + break; + + case State::Path: + if ($this->isSearchPrefix()) { + $this->changeState(State::Query, 1); + } elseif ($this->isHashPrefix()) { + $this->changeState(State::Fragment, 1); + } + + break; + + case State::Query: + if ($this->isHashPrefix()) { + $this->changeState(State::Fragment, 1); + } + + break; + + case State::Fragment: + case State::Done: + break; + } + } + + // If hostname was specified but port wasn't, the parser produces + // an empty port component. + if ($this->hasComponent(State::Host->value) && ! $this->hasComponent(State::Port->value)) { + $this->components[State::Port->value] = ''; + } + + return $this->components; + } + + private function changeState(State $newState, int $skip): void + { + match ($this->state) { + State::Init, State::Authority, State::Done => null, + default => $this->setComponent($this->state), + }; + + if (State::Init !== $this->state && State::Done !== $newState) { + if ( + in_array($this->state, [State::Scheme, State::Authority, State::Username, State::Password], true) + && in_array($newState, [State::Port, State::Path, State::Query, State::Fragment], true) + ) { + $this->components[State::Host->value] ??= ''; + } + + if ( + in_array($this->state, [State::Scheme, State::Authority, State::Username, State::Password, State::Host, State::Port], true) + && in_array($newState, [State::Query, State::Fragment], true) + ) { + $this->components[State::Path->value] ??= $this->shouldTreatAsStandardUrl ? '/' : ''; + } + + if ( + in_array($this->state, [State::Scheme, State::Authority, State::Username, State::Password, State::Host, State::Port, State::Path], true) + && State::Fragment === $newState + ) { + $this->components[State::Query->value] ??= ''; + } + } + + $this->changeStateWithoutSettingComponent($newState, $skip); + } + + private function changeStateWithoutSettingComponent( + State $newState, + int $skip, + ): void { + $this->state = $newState; + $this->componentStart = $this->tokenIndex + $skip; + $this->tokenIndex += $skip; + $this->tokenIncrement = 0; + } + + private function rewind(): void + { + $this->tokenIndex = $this->componentStart; + $this->tokenIncrement = 0; + } + + private function rewindAndSetState(State $newState): void + { + $this->rewind(); + $this->state = $newState; + } + + private function currentToken(): Token + { + return $this->safeToken($this->tokenIndex); + } + + private function safeToken(int $index): Token + { + if ($index < 0) { + $index = count($this->tokenList) + $index; + } + + return $this->tokenList[ + min($index, count($this->tokenList) - 1) + ]; + } + + private function isNonSpecialPatternChar( + int $index, + string $value, + ): bool { + $token = $this->safeToken($index); + + return $token->value === $value + && in_array($token->type, [TokenType::Char, TokenType::EscapedChar, TokenType::InvalidChar], true); + } + + private function isProtocolSuffix(): bool + { + return $this->isNonSpecialPatternChar($this->tokenIndex, ':'); + } + + private function nextIsAuthoritySlashes(): bool + { + return $this->isNonSpecialPatternChar($this->tokenIndex + 1, '/') + && $this->isNonSpecialPatternChar($this->tokenIndex + 2, '/'); + } + + private function isIdentityTerminator(): bool + { + return $this->isNonSpecialPatternChar($this->tokenIndex, '@'); + } + + private function isPasswordPrefix(): bool + { + return $this->isNonSpecialPatternChar($this->tokenIndex, ':'); + } + + private function isPortPrefix(): bool + { + return $this->isNonSpecialPatternChar($this->tokenIndex, ':'); + } + + private function isPathnameStart(): bool + { + return $this->isNonSpecialPatternChar($this->tokenIndex, '/'); + } + + private function isSearchPrefix(): bool + { + if ($this->isNonSpecialPatternChar($this->tokenIndex, '?')) { + return true; + } + + if ('?' !== $this->currentToken()->value) { + return false; + } + + $previousToken = $this->safeToken($this->tokenIndex - 1); + + return ! in_array($previousToken->type, [ + TokenType::Name, + TokenType::Regex, + TokenType::Close, + TokenType::Asterisk, + ], true); + } + + private function isHashPrefix(): bool + { + return $this->isNonSpecialPatternChar($this->tokenIndex, '#'); + } + + private function isGroupOpen(): bool + { + return TokenType::Open === $this->currentToken()->type; + } + + private function isGroupClose(): bool + { + return TokenType::Close === $this->currentToken()->type; + } + + private function isIPv6Open(): bool + { + return $this->isNonSpecialPatternChar($this->tokenIndex, '['); + } + + private function isIPv6Close(): bool + { + return $this->isNonSpecialPatternChar($this->tokenIndex, ']'); + } + + private function setComponent(State $name): void + { + $this->components[$name->value] = Component::fromPattern($this->makeComponentString())->pattern; + } + + private function makeComponentString(): string + { + $token = $this->currentToken(); + $componentCharStart = $this->safeToken($this->componentStart)->index; + + return substr( + $this->input, + $componentCharStart, + $token->index - $componentCharStart, + ); + } + + private function hasComponent(string $name): bool + { + return array_key_exists($name, $this->components); + } + + private function computeShouldTreatAsStandardUrl(): void + { + $regexp = PathToRegexp::stringToRegexp( + $this->makeComponentString(), + options: [ + 'delimiter' => '', + 'prefixes' => '', + 'sensitive' => true, + 'strict' => true, + 'encodePart' => self::protocolEncodeCallback(...), + ], + ); + + $this->shouldTreatAsStandardUrl = self::isSpecialScheme($regexp); + } + + private static function isSpecialScheme(string $regexp): bool + { + foreach (self::SPECIAL_SCHEMES as $scheme) { + if (1 === preg_match('~'.$regexp.'~', $scheme)) { + return true; + } + } + + return false; + } + + private static function protocolEncodeCallback(string $input): string + { + return match (true) { + '' === $input => '', + 1 === preg_match('/^[-+.A-Za-z0-9]*$/', $input) => strtolower($input), + default => throw new TypeError("Invalid protocol '{$input}'."), + }; + } +} diff --git a/upload/api/vendor/league/uri/UrlPattern/Part.php b/upload/api/vendor/league/uri/UrlPattern/Part.php new file mode 100644 index 00000000..71628485 --- /dev/null +++ b/upload/api/vendor/league/uri/UrlPattern/Part.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\UrlPattern; + +final class Part +{ + public function __construct( + public readonly PartType $type, + public readonly string|int $name, + public readonly string $prefix, + public readonly string $value, + public readonly string $suffix, + public readonly Modifier $modifier, + ) { + } +} diff --git a/upload/api/vendor/league/uri/UrlPattern/PartType.php b/upload/api/vendor/league/uri/UrlPattern/PartType.php new file mode 100644 index 00000000..c73ed4c3 --- /dev/null +++ b/upload/api/vendor/league/uri/UrlPattern/PartType.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\UrlPattern; + +enum PartType +{ + case FullWildcard; + case SegmentWildcard; + case Regex; + case Fixed; + + public function hasVariable(): bool + { + return self::Fixed !== $this; + } + + public function hasRegexpGroup(): bool + { + return self::Regex === $this; + } +} diff --git a/upload/api/vendor/league/uri/UrlPattern/PathToRegexp.php b/upload/api/vendor/league/uri/UrlPattern/PathToRegexp.php new file mode 100644 index 00000000..e340414f --- /dev/null +++ b/upload/api/vendor/league/uri/UrlPattern/PathToRegexp.php @@ -0,0 +1,550 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\UrlPattern; + +use League\Uri\Exceptions\SyntaxError; + +use function count; +use function ord; +use function preg_match; +use function preg_replace; +use function sprintf; +use function strlen; + +final class PathToRegexp +{ + /** + * @return list + */ + public static function lex(string $input, bool $lenient = false): array + { + $tokens = []; + $length = strlen($input); + $i = 0; + + $errorOrInvalid = static function (string $message) use (&$tokens, &$i, $input, $lenient): void { + $lenient || throw new SyntaxError($message); + $tokens[] = new Token(TokenType::InvalidChar, $input[$i], $i++); + }; + + while ($i < $length) { + $char = $input[$i]; + + if ('*' === $char) { + $tokens[] = new Token(TokenType::Asterisk, $input[$i], $i++); + continue; + } + + if ('+' === $char || '?' === $char) { + $tokens[] = new Token(TokenType::OtherModifier, $input[$i], $i++); + continue; + } + + if ('\\' === $char) { + $tokens[] = new Token(TokenType::EscapedChar, $input[$i + 1] ?? '', $i++); + ++$i; + continue; + } + + if ('{' === $char) { + $tokens[] = new Token(TokenType::Open, $input[$i], $i++); + continue; + } + + if ('}' === $char) { + $tokens[] = new Token(TokenType::Close, $input[$i], $i++); + continue; + } + + if (':' === $char) { + $name = ''; + $j = $i + 1; + + while ($j < $length) { + $code = $input[$j]; + + if ( + ($j === $i + 1 && self::isIdentifierStart($code)) + || ($j !== $i + 1 && self::isIdentifierPart($code)) + ) { + $name .= $code; + ++$j; + continue; + } + + break; + } + + if ('' === $name) { + $errorOrInvalid("Missing parameter name at {$i}"); + continue; + } + + $tokens[] = new Token(TokenType::Name, $name, $i); + $i = $j; + + continue; + } + + if ('(' === $char) { + $count = 1; + $pattern = ''; + $j = $i + 1; + $error = false; + + if ($j < $length && '?' === $input[$j]) { + $errorOrInvalid('Pattern cannot start with "?" at '.$j); + continue; + } + + while ($j < $length) { + $char = $input[$j]; + + if (!self::isAscii($char)) { + $errorOrInvalid("Invalid character '{$char}' at {$j}."); + $error = true; + break; + } + + if ('\\' === $char) { + $pattern .= $input[$j++]; + if ($j < $length) { + $pattern .= $input[$j++]; + } + continue; + } + + if (')' === $char) { + --$count; + + if (0 === $count) { + ++$j; + break; + } + } elseif ('(' === $char) { + ++$count; + + if ($j + 1 < $length && '?' !== $input[$j + 1]) { + $errorOrInvalid("Capturing groups are not allowed at {$j}"); + $error = true; + break; + } + } + + $pattern .= $input[$j++]; + } + + if ($error) { + continue; + } + + if ($count > 0) { + $errorOrInvalid("Unbalanced pattern at {$i}"); + continue; + } + + if ('' === $pattern) { + $errorOrInvalid("Missing pattern at {$i}"); + continue; + } + + $tokens[] = new Token(TokenType::Regex, $pattern, $i); + $i = $j; + + continue; + } + + $tokens[] = new Token(TokenType::Char, $input[$i], $i++); + } + + $tokens[] = new Token(TokenType::End, '', $i); + + return $tokens; + } + /** + * @return list + */ + public static function parse( + string $input, + string $delimiter = '/#?', + string $prefixes = './', + ): array { + $tokens = self::lex($input); + + $segmentWildcardRegex = '[^'.self::escapeString($delimiter).']+?'; + + $result = []; + $key = 0; + $index = 0; + $pendingFixedValue = ''; + $names = []; + + $tryConsume = static function (TokenType $type) use (&$tokens, &$index): ?string { + if ($index < count($tokens) && $tokens[$index]->type === $type) { + return $tokens[$index++]->value; + } + + return null; + }; + + $tryConsumeModifier = static function () use (&$tryConsume): ?string { + return $tryConsume(TokenType::OtherModifier) + ?? $tryConsume(TokenType::Asterisk); + }; + + $mustConsume = static function (TokenType $type) use ( + &$tokens, + &$index, + &$tryConsume, + ): string { + $value = $tryConsume($type); + + if (null !== $value) { + return $value; + } + + $token = $tokens[$index]; + + throw new SyntaxError( + sprintf( + 'Unexpected %s at %d, expected %s', + $token->type->name, + $token->index, + $type->name, + ), + ); + }; + + $consumeText = static function () use (&$tryConsume): string { + $result = ''; + + while (null !== ($value = $tryConsume(TokenType::Char)) + || null !== ($value = $tryConsume(TokenType::EscapedChar)) + ) { + $result .= $value; + } + + return $result; + }; + + $maybeAddPartFromPendingFixedValue = static function () use ( + &$pendingFixedValue, + &$result, + ): void { + /* @phpstan-ignore-next-line */ + if ('' !== $pendingFixedValue) { + $result[] = new Part(PartType::Fixed, '', '', $pendingFixedValue, '', Modifier::None); + $pendingFixedValue = ''; + } + }; + + $addPart = static function ( + string $prefix, + ?string $nameToken, + ?string $regexOrWildcardToken, + string $suffix, + ?string $modifierToken, + ) use ( + &$result, + &$key, + &$names, + &$pendingFixedValue, + &$maybeAddPartFromPendingFixedValue, + $segmentWildcardRegex, + ): void { + $modifier = match ($modifierToken) { + '?' => Modifier::Optional, + '*' => Modifier::ZeroOrMore, + '+' => Modifier::OneOrMore, + default => Modifier::None, + }; + + if (null === $nameToken && null === $regexOrWildcardToken && Modifier::None === $modifier) { + $pendingFixedValue .= $prefix; + + return; + } + + $maybeAddPartFromPendingFixedValue(); + + if (null === $nameToken && null === $regexOrWildcardToken) { + if ('' === $prefix) { + return; + } + + $result[] = new Part(PartType::Fixed, '', '', $prefix, '', $modifier); + + return; + } + + $regexValue = match (true) { + null === $regexOrWildcardToken => $segmentWildcardRegex, + '*' === $regexOrWildcardToken => '.*', + default => $regexOrWildcardToken, + }; + + $type = match ($regexValue) { + $segmentWildcardRegex => PartType::SegmentWildcard, + '.*' => PartType::FullWildcard, + default => PartType::Regex, + }; + + if (PartType::Regex !== $type) { + $regexValue = ''; + } + + $name = $nameToken; + + if (null === $name && null !== $regexOrWildcardToken) { + $name = $key++; + } + + !isset($names[$name]) || throw new SyntaxError(sprintf("Duplicate name '%s'.", $name)); + + $names[$name] = true; + $result[] = new Part($type, (string) $name, $prefix, $regexValue, $suffix, $modifier); + }; + + while ($index < count($tokens)) { + $charToken = $tryConsume(TokenType::Char); + $nameToken = $tryConsume(TokenType::Name); + $regexOrWildcardToken = $tryConsume(TokenType::Regex); + + if (null === $nameToken && null === $regexOrWildcardToken) { + $regexOrWildcardToken = $tryConsume(TokenType::Asterisk); + } + + if (null !== $nameToken || null !== $regexOrWildcardToken) { + $prefix = $charToken ?? ''; + if (! str_contains($prefixes, $prefix)) { + $pendingFixedValue .= $prefix; + $prefix = ''; + } + + $maybeAddPartFromPendingFixedValue(); + $modifierToken = $tryConsumeModifier(); + $addPart($prefix, $nameToken, $regexOrWildcardToken, '', $modifierToken); + + continue; + } + + $value = $charToken ?? $tryConsume(TokenType::EscapedChar); + if (null !== $value) { + $pendingFixedValue .= $value; + + continue; + } + + $openToken = $tryConsume(TokenType::Open); + + if (null !== $openToken) { + $prefix = $consumeText(); + $nameToken = $tryConsume(TokenType::Name); + $regexOrWildcardToken = $tryConsume(TokenType::Regex); + if (null === $nameToken && null === $regexOrWildcardToken) { + $regexOrWildcardToken = $tryConsume(TokenType::Asterisk); + } + + $suffix = $consumeText(); + $mustConsume(TokenType::Close); + $modifierToken = $tryConsumeModifier(); + $addPart($prefix, $nameToken, $regexOrWildcardToken, $suffix, $modifierToken); + continue; + } + + $maybeAddPartFromPendingFixedValue(); + $mustConsume(TokenType::End); + } + + return $result; + } + + /** + * @param list $parts + * @param list|null $names + */ + public static function partsToRegexp( + array $parts, + ?array &$names = null, + array $options = [], + ): string { + $delimiter = $options['delimiter'] ?? '/#?'; + $strict = $options['strict'] ?? false; + $end = $options['end'] ?? true; + $start = $options['start'] ?? true; + + $endsWith = ''; + + $result = $start ? '^' : ''; + + foreach ($parts as $part) { + if (PartType::Fixed === $part->type) { + if (Modifier::None === $part->modifier) { + $result .= self::escapeString($part->value); + } else { + $result .= '(?:' + .self::escapeString($part->value) + .')' + .$part->modifier->value; + } + + continue; + } + + if (null !== $names) { + $names[] = $part->name; + } + + $segmentWildcardRegex = '[^' + .self::escapeString($delimiter) + .']+?'; + + $regexValue = match ($part->type) { + PartType::SegmentWildcard => $segmentWildcardRegex, + PartType::FullWildcard => '.*', + PartType::Regex => $part->value, + }; + + if ('' === $part->prefix && '' === $part->suffix) { + if ( + Modifier::None === $part->modifier + || Modifier::Optional === $part->modifier + ) { + $result .= '(' + .$regexValue + .')' + .$part->modifier->value; + } else { + $result .= '((?:' + .$regexValue + .')' + .$part->modifier->value + .')'; + } + + continue; + } + + if ( + Modifier::None === $part->modifier + || Modifier::Optional === $part->modifier + ) { + $result .= '(?:' + .self::escapeString($part->prefix) + .'(' + .$regexValue + .')' + .self::escapeString($part->suffix) + .')' + .$part->modifier->value; + + continue; + } + + $result .= '(?:' + .self::escapeString($part->prefix) + .'((?:' + .$regexValue + .')(?:' + .self::escapeString($part->suffix) + .self::escapeString($part->prefix) + .'(?:' + .$regexValue + .'))*)' + .self::escapeString($part->suffix) + .')'; + + if (Modifier::ZeroOrMore === $part->modifier) { + $result .= '?'; + } + } + + $endsWithRegex = '['.self::escapeString($endsWith).']|$'; + $delimiterRegex = '['.self::escapeString($delimiter).']'; + + if ($end) { + if (! $strict) { + $result .= $delimiterRegex.'?'; + } + + /* @phpstan-ignore-next-line */ + return '' === $endsWith ? $result.'$' : $result.'(?='.$endsWithRegex.')'; + } + + if (! $strict) { + $result .= '(?:' + .$delimiterRegex + .'(?=' + .$endsWithRegex + .'))?'; + } + + $isEndDelimited = false; + + if ([] !== $parts) { + $lastPart = $parts[array_key_last($parts)]; + + if ( + PartType::Fixed === $lastPart->type + && Modifier::None === $lastPart->modifier + ) { + $isEndDelimited = str_contains( + $delimiter, + $lastPart->value, + ); + } + } + + if (! $isEndDelimited) { + $result .= '(?=' + .$delimiterRegex + .'|' + .$endsWithRegex + .')'; + } + + return $result; + } + + /** + * @param list|null $names + */ + public static function stringToRegexp( + string $path, + ?array &$names = null, + array $options = [], + ): string { + return self::partsToRegexp(self::parse($path), $names, $options); + } + + private static function isAscii(string $char): bool + { + return ord($char) <= 0x7f; + } + + private static function isIdentifierStart(string $char): bool + { + return 1 === preg_match('/^[\p{L}_$]$/u', $char); + } + + private static function isIdentifierPart(string $char): bool + { + return 1 === preg_match('/^[\p{L}\p{N}_$]$/u', $char); + } + + private static function escapeString(string $value): string + { + return (string) preg_replace('/([.+*?^${}()[\]|\/\\\\])/', '\\\\$1', $value); + } +} diff --git a/upload/api/vendor/league/uri/UrlPattern/PatternType.php b/upload/api/vendor/league/uri/UrlPattern/PatternType.php new file mode 100644 index 00000000..abc9684e --- /dev/null +++ b/upload/api/vendor/league/uri/UrlPattern/PatternType.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\UrlPattern; + +enum PatternType +{ + case Absolute; + case Relative; +} diff --git a/upload/api/vendor/league/uri/UrlPattern/Result.php b/upload/api/vendor/league/uri/UrlPattern/Result.php new file mode 100644 index 00000000..56004a12 --- /dev/null +++ b/upload/api/vendor/league/uri/UrlPattern/Result.php @@ -0,0 +1,87 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\UrlPattern; + +use BackedEnum; +use Stringable; +use Uri\Rfc3986\Uri as Rfc3986Uri; +use Uri\WhatWg\Url as WhatWgUrl; + +final class Result +{ + private readonly bool $hasValue; + + /** + * @param list $inputs + */ + private function __construct( + public readonly ComponentResult $scheme, + public readonly ComponentResult $username, + public readonly ComponentResult $password, + public readonly ComponentResult $host, + public readonly ComponentResult $port, + public readonly ComponentResult $path, + public readonly ComponentResult $query, + public readonly ComponentResult $fragment, + public readonly array $inputs, + ) { + $this->hasValue = $this->scheme->hasValue() + || $this->username->hasValue() + || $this->password->hasValue() + || $this->host->hasValue() + || $this->port->hasValue() + || $this->path->hasValue() + || $this->query->hasValue() + || $this->fragment->hasValue(); + } + + /** + * Tells whether the Resul contain any variable. + */ + public function hasValue(): bool + { + return $this->hasValue; + } + + /** + * @param array $extraction + */ + public static function tryFrom(array $extraction, Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string $input): ?self + { + foreach ($extraction as $name => $value) { + if (!$value instanceof ComponentResult || null === ComponentName::tryFrom($name)) { + return null; + } + } + + $empty = ComponentResult::empty(); + + return new self( + scheme: $extraction[ComponentName::Scheme->value] ?? $empty, + username: $extraction[ComponentName::Username->value] ?? $empty, + password: $extraction[ComponentName::Password->value] ?? $empty, + host: $extraction[ComponentName::Host->value] ?? $empty, + port: $extraction[ComponentName::Port->value] ?? $empty, + path: $extraction[ComponentName::Path->value] ?? $empty, + query: $extraction[ComponentName::Query->value] ?? $empty, + fragment: $extraction[ComponentName::Fragment->value] ?? $empty, + inputs: [match (true) { + $input instanceof Rfc3986Uri => $input->toString(), + $input instanceof WhatWgUrl => $input->toAsciiString(), + $input instanceof BackedEnum => (string) $input->value, + default => (string) $input, + }], + ); + } +} diff --git a/upload/api/vendor/league/uri/UrlPattern/State.php b/upload/api/vendor/league/uri/UrlPattern/State.php new file mode 100644 index 00000000..99a78972 --- /dev/null +++ b/upload/api/vendor/league/uri/UrlPattern/State.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\UrlPattern; + +enum State: string +{ + case Init = 'init'; + case Scheme = 'scheme'; + case Authority = 'authority'; + case Username = 'user'; + case Password = 'pass'; + case Host = 'host'; + case Port = 'port'; + case Path = 'path'; + case Query = 'query'; + case Fragment = 'fragment'; + case Done = 'done'; +} diff --git a/upload/api/vendor/league/uri/UrlPattern/Token.php b/upload/api/vendor/league/uri/UrlPattern/Token.php new file mode 100644 index 00000000..74ffdb5b --- /dev/null +++ b/upload/api/vendor/league/uri/UrlPattern/Token.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\UrlPattern; + +final class Token +{ + public function __construct( + public readonly TokenType $type, + public readonly string $value, + public readonly int $index, + ) { + } +} diff --git a/upload/api/vendor/league/uri/UrlPattern/TokenType.php b/upload/api/vendor/league/uri/UrlPattern/TokenType.php new file mode 100644 index 00000000..e74d4f32 --- /dev/null +++ b/upload/api/vendor/league/uri/UrlPattern/TokenType.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri\UrlPattern; + +enum TokenType +{ + case Open; + case Close; + case Regex; + case Name; + case Char; + case EscapedChar; + case OtherModifier; + case InvalidChar; + case Asterisk; + case End; +} diff --git a/upload/api/vendor/league/uri/UrlPatternBuilder.php b/upload/api/vendor/league/uri/UrlPatternBuilder.php new file mode 100644 index 00000000..8167cc5f --- /dev/null +++ b/upload/api/vendor/league/uri/UrlPatternBuilder.php @@ -0,0 +1,336 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace League\Uri; + +use BackedEnum; +use League\Uri\Contracts\Conditionable; +use League\Uri\Contracts\Transformable; +use League\Uri\Contracts\UriInterface; +use League\Uri\Exceptions\SyntaxError; +use League\Uri\UrlPattern\Component; +use League\Uri\UrlPattern\ComponentName; +use League\Uri\UrlPattern\MatchMode; +use League\Uri\UrlPattern\Parser; +use League\Uri\UrlPattern\PatternType; +use Psr\Http\Message\UriInterface as Psr7UriInterface; +use Stringable; +use Throwable; +use Uri\Rfc3986\Uri as Rfc3986Uri; +use Uri\WhatWg\Url as WhatWgUrl; + +use function array_filter; +use function is_bool; +use function strrpos; +use function substr; + +final class UrlPatternBuilder implements Conditionable, Transformable +{ + private ?Component $scheme = null; + private ?Component $username = null; + private ?Component $password = null; + private ?Component $host = null; + private ?Component $port = null; + private ?Component $path = null; + private ?Component $query = null; + private ?Component $fragment = null; + private Rfc3986Uri|WhatWgUrl|Psr7UriInterface|UriInterface|null $baseUrl = null; + + public function __construct( + Component|BackedEnum|Stringable|string|null $scheme = null, + Component|BackedEnum|Stringable|string|null $username = null, + Component|BackedEnum|Stringable|string|null $password = null, + Component|BackedEnum|Stringable|string|null $host = null, + Component|BackedEnum|Stringable|string|null $port = null, + Component|BackedEnum|Stringable|string|null $path = null, + Component|BackedEnum|Stringable|string|null $query = null, + Component|BackedEnum|Stringable|string|null $fragment = null, + Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string|null $baseUrl = null + ) { + $this + ->scheme($scheme) + ->username($username) + ->password($password) + ->host($host) + ->port($port) + ->path($path) + ->query($query) + ->fragment($fragment) + ->baseUrl($baseUrl); + } + + public static function from( + Stringable|string $pattern, + Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string|null $baseUrl = null + ): self { + $components = (new Parser((string) $pattern))->components(); + + return new self( + scheme: $components[ComponentName::Scheme->value] ?? null, + username: $components[ComponentName::Username->value] ?? null, + password: $components[ComponentName::Password->value] ?? null, + host: $components[ComponentName::Host->value] ?? null, + port: $components[ComponentName::Port->value] ?? null, + path: $components[ComponentName::Path->value] ?? null, + query: $components[ComponentName::Query->value] ?? null, + fragment: $components[ComponentName::Fragment->value] ?? null, + baseUrl: $baseUrl, + ); + } + + /** + * Tells whether the URLPattern can be built with the current Builder state. + */ + public function validate(?PatternType $type = null): bool + { + try { + $this->build(type: $type); + + return true; + } catch (Throwable) { + return false; + } + } + + /** + * @throws SyntaxError if the URLPattern can not be build with the current Builder state + */ + public function guard(?PatternType $type = null): self + { + try { + $this->build(type: $type); + + return $this; + } catch (Throwable $exception) { + throw new SyntaxError('The current builder cannot generate a valid URL Pattern.', previous: $exception); + } + } + + /** + * @throws SyntaxError + */ + public function build( + MatchMode $matchMode = MatchMode::CaseSensitive, + ?PatternType $type = null + ): UrlPattern { + $components = array_filter( + $this->applyBaseUrl([ + ComponentName::Scheme->value => $this->scheme ?? null, + ComponentName::Username->value => $this->username ?? null, + ComponentName::Password->value => $this->password ?? null, + ComponentName::Host->value => $this->host ?? null, + ComponentName::Port->value => $this->port ?? null, + ComponentName::Path->value => $this->path ?? null, + ComponentName::Query->value => $this->query ?? null, + ComponentName::Fragment->value => $this->fragment ?? null, + ]), + static fn (?Component $component): bool => $component instanceof Component + ); + + return match ($type) { + null => new UrlPattern($components, $matchMode), + PatternType::Relative => isset($components[ComponentName::Scheme->value]) + ? throw new SyntaxError('An relative URL pattern must not contain a scheme component.') + : new UrlPattern($components, $matchMode), + default => isset($components[ComponentName::Scheme->value]) + ? new UrlPattern($components, $matchMode) + : throw new SyntaxError('An absolute URL pattern must contain a scheme component.'), + }; + } + + /** + * @return array + */ + private function applyBaseUrl(array $components): array + { + if (null === $this->baseUrl) { + return $components; + } + + $hasScheme = isset($components[ComponentName::Scheme->value]); + $hasHost = isset($components[ComponentName::Host->value]); + $hasPort = isset($components[ComponentName::Port->value]); + + if (! $hasScheme) { + $baseScheme = (string) $this->baseUrl->getScheme(); + $components[ComponentName::Scheme->value] = '' !== $baseScheme + ? Component::fromPattern($baseScheme) + : Component::fromAsterisk(); + } + + if (! $hasScheme && ! $hasHost) { + $baseHost = (string) ($this->baseUrl instanceof WhatWgUrl ? $this->baseUrl->getAsciiHost() : $this->baseUrl->getHost()); + $components[ComponentName::Host->value] = '' !== $baseHost + ? Component::fromPattern($baseHost) + : Component::fromAsterisk(); + } + + if (! $hasPort && ! $hasScheme && ! $hasHost) { + $basePort = $this->baseUrl->getPort(); + $components[ComponentName::Port->value] = null !== $basePort + ? Component::fromPattern((string) $basePort) + : Component::fromEmpty(); + } + + $basePath = $this->baseUrl->getPath(); + $components[ComponentName::Path->value] = match (true) { + !isset($components[ComponentName::Path->value]) => '' !== $basePath ? Component::fromPattern($basePath) : Component::fromAsterisk(), + default => self::resolvePathname($components[ComponentName::Path->value], $basePath), + }; + + return $components; + } + + private static function resolvePathname(Component $pathname, string $basePathname): Component + { + if (str_starts_with($pathname->pattern, '/')) { + return $pathname; + } + + $slash = strrpos($basePathname, '/'); + + $pattern = false === $slash + ? $pathname->pattern + : substr($basePathname, 0, $slash + 1).$pathname->pattern; + + return Component::fromPattern($pattern); + } + + private static function filter(Component|BackedEnum|Stringable|string|null $component): ?Component + { + return match (true) { + $component instanceof Component => $component, + null === $component => null, + $component instanceof BackedEnum => Component::fromPattern((string) $component->value), + default => Component::fromPattern((string) $component), + }; + } + + public function scheme(Component|BackedEnum|Stringable|string|null $scheme): self + { + $this->scheme = self::filter($scheme); + + return $this; + } + + public function username(Component|BackedEnum|Stringable|string|null $username): self + { + $this->username = self::filter($username); + + return $this; + } + + public function password(Component|BackedEnum|Stringable|string|null $password): self + { + $this->password = self::filter($password); + + return $this; + } + + public function host(Component|BackedEnum|Stringable|string|null $host): self + { + $this->host = self::filter($host); + + return $this; + } + + public function port(Component|BackedEnum|Stringable|string|null $port): self + { + $this->port = self::filter($port); + + return $this; + } + + public function path(Component|BackedEnum|Stringable|string|null $path): self + { + $this->path = self::filter($path); + + return $this; + } + + public function query(Component|BackedEnum|Stringable|string|null $query): self + { + $this->query = self::filter($query); + + return $this; + } + + public function fragment(Component|BackedEnum|Stringable|string|null $fragment): self + { + $this->fragment = self::filter($fragment); + + return $this; + } + + public function baseUrl(Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string|null $baseUrl = null): self + { + if (null === $baseUrl) { + $this->baseUrl = null; + + return $this; + } + + $baseUrl = match (true) { + $baseUrl instanceof Rfc3986Uri, + $baseUrl instanceof WhatWgUrl, + $baseUrl instanceof UriInterface, + $baseUrl instanceof Psr7UriInterface => $baseUrl, + default => Uri::new($baseUrl), + }; + + '' !== (string) $baseUrl->getScheme() || throw new SyntaxError('The BaseUrl must represent an absolute URI.'); + + $this->baseUrl = $baseUrl; + + return $this; + } + + public function reset(): self + { + $this->scheme = null; + $this->username = null; + $this->password = null; + $this->host = null; + $this->port = null; + $this->path = null; + $this->query = null; + $this->fragment = null; + $this->baseUrl = null; + + return $this; + } + + /** + * Executes the given callback with the current instance + * and returns the current instance. + * + * @param callable(self): self $callback + */ + public function transform(callable $callback): static + { + return $callback($this); + } + + public function when(callable|bool $condition, callable $onSuccess, ?callable $onFail = null): static + { + if (!is_bool($condition)) { + $condition = $condition($this); + } + + return match (true) { + $condition => $onSuccess($this), + null !== $onFail => $onFail($this), + default => $this, + } ?? $this; + } +} diff --git a/upload/api/vendor/league/uri/Urn.php b/upload/api/vendor/league/uri/Urn.php index f5f9f619..898f88cc 100644 --- a/upload/api/vendor/league/uri/Urn.php +++ b/upload/api/vendor/league/uri/Urn.php @@ -131,8 +131,8 @@ public static function fromString(Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|str return new self( nid: $matches['nid'], nss: $matches['nss'], - rComponent: (isset($matches['frc']) && '' !== $matches['frc']) ? $matches['rcomponent'] : null, - qComponent: (isset($matches['fqc']) && '' !== $matches['fqc']) ? $matches['qcomponent'] : null, + rComponent: (isset($matches['frc'], $matches['rcomponent']) && '' !== $matches['frc']) ? $matches['rcomponent'] : null, + qComponent: (isset($matches['fqc'], $matches['qcomponent']) && '' !== $matches['fqc']) ? $matches['qcomponent'] : null, fComponent: $matches['fcomponent'] ?? null, ); } diff --git a/upload/api/vendor/league/uri/composer.json b/upload/api/vendor/league/uri/composer.json index 3ab4fd84..c9d3687a 100644 --- a/upload/api/vendor/league/uri/composer.json +++ b/upload/api/vendor/league/uri/composer.json @@ -48,8 +48,9 @@ ], "require": { "php": "^8.1", - "league/uri-interfaces": "^7.8.1", - "psr/http-factory": "^1" + "league/uri-interfaces": "^7.9.1", + "psr/http-factory": "^1", + "ext-mbstring": "*" }, "autoload": { "psr-4": { diff --git a/upload/api/vendor/phpstan/phpdoc-parser/.agents/setup b/upload/api/vendor/phpstan/phpdoc-parser/.agents/setup new file mode 100755 index 00000000..619b6eaa --- /dev/null +++ b/upload/api/vendor/phpstan/phpdoc-parser/.agents/setup @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +# Prepares an Amp orb (Debian 12) to build, test, and analyse this repository. +# Idempotent: a warm run skips installs that are already in place. +set -euo pipefail + +cd "$(dirname "$0")/.." + +step() { + local name="$1" + shift + local start=$SECONDS + echo "==> $name" + "$@" + echo "<== $name ($((SECONDS - start))s)" +} + +# PHP 8.4 runs everything: the library itself (platform pinned to 7.4.6 in +# composer.json), and tools/phplrt, whose grammar compiler requires 8.4. +PHP_VERSION=8.4 +PHP_PACKAGES=( + "php$PHP_VERSION-cli" + "php$PHP_VERSION-mbstring" + "php$PHP_VERSION-xml" + "php$PHP_VERSION-curl" + "php$PHP_VERSION-zip" +) + +install_php() { + local missing=() + for pkg in "${PHP_PACKAGES[@]}"; do + dpkg-query -W -f='${Status}' "$pkg" 2>/dev/null | grep -q 'install ok installed' || missing+=("$pkg") + done + if [ ${#missing[@]} -eq 0 ]; then + echo "PHP $PHP_VERSION packages already installed" + return + fi + + if [ ! -f /etc/apt/sources.list.d/php-sury.list ]; then + sudo install -d -m 0755 /etc/apt/keyrings + curl -fsSL https://packages.sury.org/php/apt.gpg | sudo tee /etc/apt/keyrings/php-sury.gpg >/dev/null + echo "deb [signed-by=/etc/apt/keyrings/php-sury.gpg] https://packages.sury.org/php/ $(. /etc/os-release && echo "$VERSION_CODENAME") main" \ + | sudo tee /etc/apt/sources.list.d/php-sury.list >/dev/null + fi + sudo apt-get update -qq + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends "${missing[@]}" +} + +install_composer() { + if command -v composer >/dev/null 2>&1; then + echo "Composer already installed: $(composer --version 2>/dev/null)" + return + fi + local tmp + tmp="$(mktemp -d)" + curl -fsSL https://getcomposer.org/installer -o "$tmp/composer-setup.php" + php "$tmp/composer-setup.php" --quiet --install-dir="$tmp" --filename=composer + sudo install -m 0755 "$tmp/composer" /usr/local/bin/composer + rm -rf "$tmp" +} + +# composer.lock is git-ignored, so the first run resolves dependencies like CI +# does; later runs reuse the restored lock and vendor directory. +install_dependencies() { + composer install --no-interaction --no-progress +} + +# Coding standard used by `make cs` / `make cs-fix`. +install_build_cs() { + if [ ! -d build-cs/.git ]; then + git clone --quiet --branch 2.x https://github.com/phpstan/build-cs.git build-cs + fi + composer install --no-interaction --no-progress --working-dir build-cs +} + +# Grammar toolchain used by FuzzyTest and GrammarSyncTest (`make grammars-install`). +install_grammars() { + composer install --no-interaction --no-progress --working-dir tools/phplrt +} + +step "Install PHP $PHP_VERSION" install_php +step "Install Composer" install_composer +step "Install dependencies" install_dependencies +step "Install build-cs" install_build_cs +step "Install grammar toolchain" install_grammars diff --git a/upload/api/vendor/phpstan/phpdoc-parser/README.md b/upload/api/vendor/phpstan/phpdoc-parser/README.md index ac370349..1913bb6c 100644 --- a/upload/api/vendor/phpstan/phpdoc-parser/README.md +++ b/upload/api/vendor/phpstan/phpdoc-parser/README.md @@ -13,7 +13,7 @@ For the complete list of supported PHPDoc features check out PHPStan documentati * [PHPDoc Basics](https://phpstan.org/writing-php-code/phpdocs-basics) (list of PHPDoc tags) * [PHPDoc Types](https://phpstan.org/writing-php-code/phpdoc-types) (list of PHPDoc types) -* [phpdoc-parser API Reference](https://phpstan.github.io/phpdoc-parser/2.3.x/namespace-PHPStan.PhpDocParser.html) with all the AST node types etc. +* [phpdoc-parser API Reference](https://phpstan.github.io/phpdoc-parser/2.4.x/namespace-PHPStan.PhpDocParser.html) with all the AST node types etc. This parser also supports parsing [Doctrine Annotations](https://github.com/doctrine/annotations). The AST nodes live in the [PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine namespace](https://phpstan.github.io/phpdoc-parser/2.1.x/namespace-PHPStan.PhpDocParser.Ast.PhpDoc.Doctrine.html). diff --git a/upload/api/vendor/phpstan/phpdoc-parser/src/Ast/PhpDoc/TypeAliasImportTagValueNode.php b/upload/api/vendor/phpstan/phpdoc-parser/src/Ast/PhpDoc/TypeAliasImportTagValueNode.php index 147513d3..d1c3c9f7 100644 --- a/upload/api/vendor/phpstan/phpdoc-parser/src/Ast/PhpDoc/TypeAliasImportTagValueNode.php +++ b/upload/api/vendor/phpstan/phpdoc-parser/src/Ast/PhpDoc/TypeAliasImportTagValueNode.php @@ -4,6 +4,8 @@ use PHPStan\PhpDocParser\Ast\NodeAttributes; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\NameNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use function trim; class TypeAliasImportTagValueNode implements PhpDocTagValueNode @@ -13,11 +15,15 @@ class TypeAliasImportTagValueNode implements PhpDocTagValueNode public string $importedAlias; - public IdentifierTypeNode $importedFrom; + /** @var IdentifierTypeNode|NameNode */ + public TypeNode $importedFrom; public ?string $importedAs = null; - public function __construct(string $importedAlias, IdentifierTypeNode $importedFrom, ?string $importedAs) + /** + * @param IdentifierTypeNode|NameNode $importedFrom + */ + public function __construct(string $importedAlias, TypeNode $importedFrom, ?string $importedAs) { $this->importedAlias = $importedAlias; $this->importedFrom = $importedFrom; diff --git a/upload/api/vendor/phpstan/phpdoc-parser/src/Ast/Type/CallableTypeNode.php b/upload/api/vendor/phpstan/phpdoc-parser/src/Ast/Type/CallableTypeNode.php index ea34a55a..0aa232bb 100644 --- a/upload/api/vendor/phpstan/phpdoc-parser/src/Ast/Type/CallableTypeNode.php +++ b/upload/api/vendor/phpstan/phpdoc-parser/src/Ast/Type/CallableTypeNode.php @@ -11,7 +11,8 @@ class CallableTypeNode implements TypeNode use NodeAttributes; - public IdentifierTypeNode $identifier; + /** @var IdentifierTypeNode|NameNode */ + public TypeNode $identifier; /** @var TemplateTagValueNode[] */ public array $templateTypes; @@ -22,10 +23,11 @@ class CallableTypeNode implements TypeNode public TypeNode $returnType; /** + * @param IdentifierTypeNode|NameNode $identifier * @param CallableTypeParameterNode[] $parameters * @param TemplateTagValueNode[] $templateTypes */ - public function __construct(IdentifierTypeNode $identifier, array $parameters, TypeNode $returnType, array $templateTypes) + public function __construct(TypeNode $identifier, array $parameters, TypeNode $returnType, array $templateTypes) { $this->identifier = $identifier; $this->parameters = $parameters; diff --git a/upload/api/vendor/phpstan/phpdoc-parser/src/Ast/Type/FullyQualifiedNameNode.php b/upload/api/vendor/phpstan/phpdoc-parser/src/Ast/Type/FullyQualifiedNameNode.php new file mode 100644 index 00000000..c8780163 --- /dev/null +++ b/upload/api/vendor/phpstan/phpdoc-parser/src/Ast/Type/FullyQualifiedNameNode.php @@ -0,0 +1,34 @@ +name = ltrim($name, '\\'); + } + + public function toCodeString(): string + { + return '\\' . $this->name; + } + + public function isFullyQualified(): bool + { + return true; + } + +} diff --git a/upload/api/vendor/phpstan/phpdoc-parser/src/Ast/Type/GenericTypeNode.php b/upload/api/vendor/phpstan/phpdoc-parser/src/Ast/Type/GenericTypeNode.php index 0069aa4d..dd6e75ae 100644 --- a/upload/api/vendor/phpstan/phpdoc-parser/src/Ast/Type/GenericTypeNode.php +++ b/upload/api/vendor/phpstan/phpdoc-parser/src/Ast/Type/GenericTypeNode.php @@ -16,7 +16,8 @@ class GenericTypeNode implements TypeNode use NodeAttributes; - public IdentifierTypeNode $type; + /** @var IdentifierTypeNode|NameNode */ + public TypeNode $type; /** @var TypeNode[] */ public array $genericTypes; @@ -25,10 +26,11 @@ class GenericTypeNode implements TypeNode public array $variances; /** + * @param IdentifierTypeNode|NameNode $type * @param TypeNode[] $genericTypes * @param (self::VARIANCE_*)[] $variances */ - public function __construct(IdentifierTypeNode $type, array $genericTypes, array $variances = []) + public function __construct(TypeNode $type, array $genericTypes, array $variances = []) { $this->type = $type; $this->genericTypes = $genericTypes; diff --git a/upload/api/vendor/phpstan/phpdoc-parser/src/Ast/Type/NameNode.php b/upload/api/vendor/phpstan/phpdoc-parser/src/Ast/Type/NameNode.php new file mode 100644 index 00000000..ca5f285c --- /dev/null +++ b/upload/api/vendor/phpstan/phpdoc-parser/src/Ast/Type/NameNode.php @@ -0,0 +1,37 @@ + + */ + public function getParts(): array; + + public function getFirst(): string; + + public function getLast(): string; + + public function isFullyQualified(): bool; + +} diff --git a/upload/api/vendor/phpstan/phpdoc-parser/src/Ast/Type/NameNodeTrait.php b/upload/api/vendor/phpstan/phpdoc-parser/src/Ast/Type/NameNodeTrait.php new file mode 100644 index 00000000..636fa44e --- /dev/null +++ b/upload/api/vendor/phpstan/phpdoc-parser/src/Ast/Type/NameNodeTrait.php @@ -0,0 +1,61 @@ +name; + } + + public function getParts(): array + { + return explode('\\', $this->name); + } + + public function getFirst(): string + { + return $this->getParts()[0]; + } + + public function getLast(): string + { + $parts = $this->getParts(); + + return $parts[count($parts) - 1]; + } + + public function __toString(): string + { + return $this->toCodeString(); + } + + /** + * @param array $properties + */ + public static function __set_state(array $properties): self + { + $instance = new self($properties['name']); + if (isset($properties['attributes'])) { + foreach ($properties['attributes'] as $key => $value) { + $instance->setAttribute($key, $value); + } + } + return $instance; + } + +} diff --git a/upload/api/vendor/phpstan/phpdoc-parser/src/Ast/Type/RelativeNameNode.php b/upload/api/vendor/phpstan/phpdoc-parser/src/Ast/Type/RelativeNameNode.php new file mode 100644 index 00000000..e776877b --- /dev/null +++ b/upload/api/vendor/phpstan/phpdoc-parser/src/Ast/Type/RelativeNameNode.php @@ -0,0 +1,37 @@ +name = $name; + } + + public function toCodeString(): string + { + return $this->name; + } + + public function isFullyQualified(): bool + { + return false; + } + +} diff --git a/upload/api/vendor/phpstan/phpdoc-parser/src/Lexer/Lexer.php b/upload/api/vendor/phpstan/phpdoc-parser/src/Lexer/Lexer.php index c470100c..01de5857 100644 --- a/upload/api/vendor/phpstan/phpdoc-parser/src/Lexer/Lexer.php +++ b/upload/api/vendor/phpstan/phpdoc-parser/src/Lexer/Lexer.php @@ -182,7 +182,7 @@ private function generateRegexp(): string self::TOKEN_OPEN_PHPDOC => '/\\*\\*(?=\\s)\\x20?+', self::TOKEN_CLOSE_PHPDOC => '\\*/', - self::TOKEN_PHPDOC_TAG => '@(?:[a-z][a-z0-9-\\\\]+:)?[a-z][a-z0-9-\\\\]*+', + self::TOKEN_PHPDOC_TAG => '@(?:[a-z][a-z0-9-\\\\]+:)?[a-z][a-z0-9_\\\\-]*+', self::TOKEN_DOCTRINE_TAG => '@[a-z_\\\\][a-z0-9_\:\\\\]*[a-z_][a-z0-9_]*', self::TOKEN_PHPDOC_EOL => '\\r?+\\n[\\x09\\x20]*+(?:\\*(?!/)\\x20?+)?', diff --git a/upload/api/vendor/phpstan/phpdoc-parser/src/Parser/PhpDocParser.php b/upload/api/vendor/phpstan/phpdoc-parser/src/Parser/PhpDocParser.php index f6734225..581a8b1e 100644 --- a/upload/api/vendor/phpstan/phpdoc-parser/src/Parser/PhpDocParser.php +++ b/upload/api/vendor/phpstan/phpdoc-parser/src/Parser/PhpDocParser.php @@ -149,11 +149,21 @@ private function parseChild(TokenIterator $tokens): Ast\PhpDoc\PhpDocChildNode $tagStartLine = $tokens->currentTokenLine(); $tagStartIndex = $tokens->currentTokenIndex(); + try { + $tokens->pushSavePoint(); + $tagValue = $this->parseDoctrineTagValue($tokens, $tag); + $tokens->dropSavePoint(); + + } catch (ParserException $e) { + $tokens->rollback(); + $tagValue = new Ast\PhpDoc\InvalidTagValueNode($this->parseOptionalDescription($tokens, false), $e); + } + return $this->enrichWithAttributes($tokens, new Ast\PhpDoc\PhpDocTagNode( $tag, $this->enrichWithAttributes( $tokens, - $this->parseDoctrineTagValue($tokens, $tag), + $tagValue, $tagStartLine, $tagStartIndex, ), @@ -577,7 +587,8 @@ private function parseDoctrineArguments(TokenIterator $tokens, bool $deep): arra private function parseDoctrineArgument(TokenIterator $tokens): Doctrine\DoctrineArgument { - if (!$tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER)) { + // a quoted key, e.g. "key"="value", is not valid Doctrine syntax, but is accepted as the key + if (!$tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER, Lexer::TOKEN_DOUBLE_QUOTED_STRING, Lexer::TOKEN_DOCTRINE_ANNOTATION_STRING)) { $startLine = $tokens->currentTokenLine(); $startIndex = $tokens->currentTokenIndex(); @@ -594,8 +605,12 @@ private function parseDoctrineArgument(TokenIterator $tokens): Doctrine\Doctrine try { $tokens->pushSavePoint(); + $isIdentifier = $tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER); $currentValue = $tokens->currentTokenValue(); - $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); + $tokens->next(); + if (!$isIdentifier) { + $currentValue = $this->doctrineConstantExprParser->parseDoctrineString($currentValue, $tokens)->value; + } $key = $this->enrichWithAttributes( $tokens, diff --git a/upload/api/vendor/phpstan/phpdoc-parser/src/Parser/TokenIterator.php b/upload/api/vendor/phpstan/phpdoc-parser/src/Parser/TokenIterator.php index 043639f1..259a6bdd 100644 --- a/upload/api/vendor/phpstan/phpdoc-parser/src/Parser/TokenIterator.php +++ b/upload/api/vendor/phpstan/phpdoc-parser/src/Parser/TokenIterator.php @@ -9,7 +9,9 @@ use function assert; use function count; use function in_array; +use function preg_match_all; use function strlen; +use function strpos; use function substr; class TokenIterator @@ -34,6 +36,9 @@ class TokenIterator /** @var list|null */ private ?array $offsets = null; + /** @var array|null the last index of a token containing "/name>", by the name */ + private ?array $endTagIndexes = null; + /** * @param list $tokens */ @@ -390,6 +395,34 @@ public function hasTokenImmediatelyAfter(int $pos, int $expectedTokenType): bool return false; } + /** + * Whether a token at the current index or after it contains "/$name>", + * which every end tag of an HTML tag of that name does. + * + * The tokens are searched once, so that asking about every "<" of a + * PHPDoc does not walk to its end every time. + * + * @internal + */ + public function hasEndTagCandidateAhead(string $name): bool + { + if ($this->endTagIndexes === null) { + $this->endTagIndexes = []; + foreach ($this->tokens as $index => $token) { + $value = $token[Lexer::VALUE_OFFSET]; + if (strpos($value, '/') === false || preg_match_all('~/([^/>]++)>~', $value, $matches) === 0) { + continue; + } + + foreach ($matches[1] as $candidate) { + $this->endTagIndexes[$candidate] = $index; + } + } + } + + return ($this->endTagIndexes[$name] ?? -1) >= $this->index; + } + public function getDetectedNewline(): ?string { return $this->newline; diff --git a/upload/api/vendor/phpstan/phpdoc-parser/src/Parser/TypeParser.php b/upload/api/vendor/phpstan/phpdoc-parser/src/Parser/TypeParser.php index a2db4fce..348c56d1 100644 --- a/upload/api/vendor/phpstan/phpdoc-parser/src/Parser/TypeParser.php +++ b/upload/api/vendor/phpstan/phpdoc-parser/src/Parser/TypeParser.php @@ -419,6 +419,11 @@ public function isHtml(TokenIterator $tokens): bool return false; } + // every end tag the walk below looks for contains this + if (!$tokens->hasEndTagCandidateAhead($htmlTagName)) { + return false; + } + $endTag = ''; $endTagSearchOffset = - strlen($endTag); diff --git a/upload/api/vendor/phpstan/phpdoc-parser/src/Printer/Printer.php b/upload/api/vendor/phpstan/phpdoc-parser/src/Printer/Printer.php index db7d3db7..61deed7e 100644 --- a/upload/api/vendor/phpstan/phpdoc-parser/src/Printer/Printer.php +++ b/upload/api/vendor/phpstan/phpdoc-parser/src/Printer/Printer.php @@ -58,6 +58,7 @@ use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode; use PHPStan\PhpDocParser\Ast\Type\InvalidTypeNode; +use PHPStan\PhpDocParser\Ast\Type\NameNode; use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode; use PHPStan\PhpDocParser\Ast\Type\ObjectShapeItemNode; use PHPStan\PhpDocParser\Ast\Type\ObjectShapeNode; @@ -69,6 +70,7 @@ use PHPStan\PhpDocParser\Parser\TokenIterator; use function array_keys; use function array_map; +use function array_values; use function assert; use function count; use function get_class; @@ -76,11 +78,14 @@ use function implode; use function in_array; use function is_array; +use function preg_match; use function preg_match_all; +use function preg_replace_callback; use function sprintf; use function str_replace; use function strlen; use function strpos; +use function substr; use function trim; use const PREG_SET_ORDER; @@ -96,6 +101,12 @@ final class Printer /** @var Differ */ private Differ $differ; + /** + * Newline, indentation and asterisk at the start of a PHPDoc line. + * Written after each newline in a printed text or description. + */ + private string $lineStart = "\n *"; + /** * Map From "{$class}->{$subNode}" to string that should be inserted * between elements of this list subnode @@ -177,6 +188,17 @@ final class Printer ]; public function printFormatPreserving(PhpDocNode $node, PhpDocNode $originalNode, TokenIterator $originalTokens): string + { + $lineStart = $this->lineStart; + $this->lineStart = $this->detectLineStart($originalTokens); + try { + return $this->printFormatPreservingWithLineStart($node, $originalNode, $originalTokens); + } finally { + $this->lineStart = $lineStart; + } + } + + private function printFormatPreservingWithLineStart(PhpDocNode $node, PhpDocNode $originalNode, TokenIterator $originalTokens): string { $this->differ = new Differ(static function ($a, $b) { if ($a instanceof Node && $b instanceof Node) { @@ -207,7 +229,7 @@ public function print(Node $node): string if ($node instanceof PhpDocNode) { return "/**\n *" . implode("\n *", array_map( function (PhpDocChildNode $child): string { - $s = $this->print($child); + $s = $this->addLineStarts($this->print($child)); return $s === '' ? '' : ' ' . $s; }, $node->children, @@ -436,7 +458,7 @@ private function printType(TypeNode $node): string ? '<' . implode(', ', array_map(fn (TemplateTagValueNode $templateNode): string => $this->print($templateNode), $node->templateTypes)) . '>' : ''; $parameters = implode(', ', array_map(fn (CallableTypeParameterNode $parameterNode): string => $this->print($parameterNode), $node->parameters)); - return "{$node->identifier}{$template}({$parameters}): {$returnType}"; + return $this->printType($node->identifier) . "{$template}({$parameters}): {$returnType}"; } if ($node instanceof ConditionalTypeForParameterNode) { return sprintf( @@ -475,11 +497,14 @@ private function printType(TypeNode $node): string } } - return $node->type . '<' . implode(', ', $genericTypes) . '>'; + return $this->printType($node->type) . '<' . implode(', ', $genericTypes) . '>'; } if ($node instanceof IdentifierTypeNode) { return $node->name; } + if ($node instanceof NameNode) { + return $node->toCodeString(); + } if ($node instanceof IntersectionTypeNode || $node instanceof UnionTypeNode) { $items = []; foreach ($node->types as $type) { @@ -578,17 +603,27 @@ private function printConstExpr(ConstExprNode $node): string */ private function printArrayFormatPreserving(array $nodes, array $originalNodes, TokenIterator $originalTokens, int &$tokenIndex, string $parentNodeClass, string $subNodeName): ?string { + // unset() leaves gaps in the keys, the differ reads the lists by position + $nodes = array_values($nodes); + $originalNodes = array_values($originalNodes); + $diff = $this->differ->diffWithReplacements($originalNodes, $nodes); $mapKey = $parentNodeClass . '->' . $subNodeName; $insertStr = $this->listInsertionMap[$mapKey] ?? null; $result = ''; $beforeFirstKeepOrReplace = true; + $skipRemovedNode = false; $delayedAdd = []; $insertNewline = false; [$isMultiline, $beforeAsteriskIndent, $afterAsteriskIndent] = $this->isMultiline($tokenIndex, $originalNodes, $originalTokens); if ($insertStr === "\n * ") { + $lastChildIndent = $this->detectLastChildIndent($originalNodes, $originalTokens); + if ($lastChildIndent !== null) { + [$beforeAsteriskIndent, $afterAsteriskIndent] = $lastChildIndent; + } + $insertStr = sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent); } @@ -618,7 +653,12 @@ private function printArrayFormatPreserving(array $nodes, array $originalNodes, $commentStartPos = count($origComments) > 0 ? $origComments[0]->startIndex : $itemStartPos; assert($commentStartPos >= 0); - $result .= $originalTokens->getContentBetween($tokenIndex, $itemStartPos); + if ($skipRemovedNode) { + // the separator after the removed first item belongs to it + $skipRemovedNode = false; + } else { + $result .= $originalTokens->getContentBetween($tokenIndex, $itemStartPos); + } if (count($delayedAdd) > 0) { foreach ($delayedAdd as $delayedAddNode) { @@ -636,7 +676,7 @@ private function printArrayFormatPreserving(array $nodes, array $originalNodes, } } - $result .= $this->printNodeFormatPreserving($delayedAddNode, $originalTokens); + $result = $this->appendNode($result, $this->printNodeFormatPreserving($delayedAddNode, $originalTokens), $afterAsteriskIndent); if ($parenthesesNeeded) { $result .= ')'; } @@ -711,7 +751,7 @@ private function printArrayFormatPreserving(array $nodes, array $originalNodes, $result .= '('; } - $result .= $this->printNodeFormatPreserving($arrItem, $originalTokens); + $result = $this->appendNode($result, $this->printNodeFormatPreserving($arrItem, $originalTokens), $afterAsteriskIndent); if ($parenthesesNeeded) { $result .= ')'; } @@ -735,12 +775,18 @@ private function printArrayFormatPreserving(array $nodes, array $originalNodes, if ($i === 0) { // If we're removing from the start, keep the tokens before the node and drop those after it, // instead of the other way around. - $originalTokensArray = $originalTokens->getTokens(); - for ($j = $tokenIndex; $j < $itemStartPos; $j++) { - if ($originalTokensArray[$j][Lexer::TYPE_OFFSET] === Lexer::TOKEN_PHPDOC_EOL) { - break; + if ($mapKey === PhpDocNode::class . '->children') { + // the line of the removed child is dropped, the next child keeps its own line + $originalTokensArray = $originalTokens->getTokens(); + for ($j = $tokenIndex; $j < $itemStartPos; $j++) { + if ($originalTokensArray[$j][Lexer::TYPE_OFFSET] === Lexer::TOKEN_PHPDOC_EOL) { + break; + } + $result .= $originalTokensArray[$j][Lexer::VALUE_OFFSET]; } - $result .= $originalTokensArray[$j][Lexer::VALUE_OFFSET]; + } else { + $result .= $originalTokens->getContentBetween($tokenIndex, $itemStartPos); + $skipRemovedNode = true; } } @@ -776,7 +822,7 @@ private function printArrayFormatPreserving(array $nodes, array $originalNodes, } } - $result .= $this->printNodeFormatPreserving($delayedAddNode, $originalTokens); + $result = $this->appendNode($result, $this->printNodeFormatPreserving($delayedAddNode, $originalTokens), $afterAsteriskIndent); $first = false; } $result .= $extraRight; @@ -785,6 +831,19 @@ private function printArrayFormatPreserving(array $nodes, array $originalNodes, return $result; } + /** + * A new empty line in a docblock is " *", without the space after the asterisk + */ + private function appendNode(string $result, string $printedNode, string $afterAsteriskIndent): string + { + $lineStart = '*' . $afterAsteriskIndent; + if ($printedNode !== '' || $afterAsteriskIndent === '' || substr($result, -strlen($lineStart)) !== $lineStart) { + return $result . $printedNode; + } + + return substr($result, 0, -strlen($afterAsteriskIndent)); + } + /** * @param list $comments */ @@ -850,12 +909,45 @@ private function isMultiline(int $initialIndex, array $nodes, TokenIterator $ori return [$isMultiline, $before, $after]; } + /** + * Indentation before and after the asterisk on the line where the last child of a PHPDoc starts. + * Other lines, e.g. the next lines of a multi-line description, can have a different indentation. + * + * @param Node[] $originalNodes + * @return array{string, string}|null + */ + private function detectLastChildIndent(array $originalNodes, TokenIterator $originalTokens): ?array + { + $tokens = $originalTokens->getTokens(); + for ($i = count($originalNodes) - 1; $i >= 0; $i--) { + $index = $originalNodes[$i]->getAttribute(Attribute::START_INDEX) - 1; + $lineStart = ''; + while (isset($tokens[$index]) && $tokens[$index][Lexer::TYPE_OFFSET] === Lexer::TOKEN_HORIZONTAL_WS) { + $lineStart = $tokens[$index][Lexer::VALUE_OFFSET] . $lineStart; + $index--; + } + + if (!isset($tokens[$index]) || $tokens[$index][Lexer::TYPE_OFFSET] !== Lexer::TOKEN_PHPDOC_EOL) { + continue; + } + + $lineStart = $tokens[$index][Lexer::VALUE_OFFSET] . $lineStart; + if (preg_match('~\n(?[\x09\x20]*)\*(?[\x09\x20]*)$~', $lineStart, $match) !== 1) { + continue; + } + + return [$match['before'], $match['after']]; + } + + return null; + } + private function printNodeFormatPreserving(Node $node, TokenIterator $originalTokens): string { /** @var Node|null $originalNode */ $originalNode = $node->getAttribute(Attribute::ORIGINAL_NODE); if ($originalNode === null) { - return $this->print($node); + return $this->printReplacement($node); } $class = get_class($node); @@ -885,6 +977,22 @@ private function printNodeFormatPreserving(Node $node, TokenIterator $originalTo continue; } + if ($this->isRenamedTag($node, $subNodeName, $originalTokens, $startPos)) { + // the tag name is the first token of the tag; keep the format of the value + $result .= $subNode; + $pos = $startPos + 1; + continue; + } + + if ( + $node instanceof DoctrineAnnotation + && $subNodeName === 'name' + && $node->getAttribute(Attribute::ORIGINAL_NODE)->name !== $originalTokens->getContentBetween($startPos, $startPos + 1) + ) { + // the tokens of the annotation of a Doctrine tag do not contain the name, the tag prints it + continue; + } + if (is_array($subNode) && is_array($origSubNode)) { // Array subnode changed, we might be able to reconstruct it $listResult = $this->printArrayFormatPreserving( @@ -897,14 +1005,14 @@ private function printNodeFormatPreserving(Node $node, TokenIterator $originalTo ); if ($listResult === null) { - return $this->print($node); + return $this->printReplacement($node); } $result .= $listResult; continue; } - return $this->print($node); + return $this->printReplacement($node); } if ($origSubNode === null) { @@ -913,7 +1021,7 @@ private function printNodeFormatPreserving(Node $node, TokenIterator $originalTo continue; } - return $this->print($node); + return $this->printReplacement($node); } $subStartPos = $origSubNode->getAttribute(Attribute::START_INDEX); @@ -923,11 +1031,16 @@ private function printNodeFormatPreserving(Node $node, TokenIterator $originalTo } if ($subEndPos < $subStartPos) { - return $this->print($node); + // the original sub-node has no tokens, e.g. a Doctrine annotation without arguments + if ($subNode !== null && $this->print($subNode) === $this->print($origSubNode)) { + continue; + } + + return $this->printReplacement($node); } if ($subNode === null) { - return $this->print($node); + return $this->printReplacement($node); } $result .= $originalTokens->getContentBetween($pos, $subStartPos); @@ -956,4 +1069,77 @@ private function printNodeFormatPreserving(Node $node, TokenIterator $originalTo return $result . $originalTokens->getContentBetween($pos, $endPos + 1); } + private function isRenamedTag(Node $node, string $subNodeName, TokenIterator $originalTokens, int $startPos): bool + { + if (!$node instanceof PhpDocTagNode || $subNodeName !== 'name') { + return false; + } + + $originalNode = $node->getAttribute(Attribute::ORIGINAL_NODE); + if (!$originalNode instanceof PhpDocTagNode) { + return false; + } + + return $originalTokens->getContentBetween($startPos, $startPos + 1) === $originalNode->name; + } + + /** + * Prints a node that replaces the tokens of the original node. + * + * The tokens of a Doctrine tag value do not contain the annotation name. + * The parent tag prints the name from its own tokens. + */ + private function printReplacement(Node $node): string + { + if (!$node instanceof DoctrineTagValueNode) { + return $this->addLineStarts($this->print($node)); + } + + $arguments = '(' . implode(', ', $node->annotation->arguments) . ')'; + if ($node->annotation->arguments === []) { + $originalNode = $node->getAttribute(Attribute::ORIGINAL_NODE); + if ( + $originalNode instanceof DoctrineTagValueNode + && $originalNode->annotation->getAttribute(Attribute::END_INDEX) < $originalNode->annotation->getAttribute(Attribute::START_INDEX) + ) { + $arguments = ''; + } + } + + return $this->addLineStarts(trim($arguments . ' ' . $node->description)); + } + + /** + * The parser removes the asterisk at the start of each line of a text or description. + * This puts it back. + */ + private function addLineStarts(string $s): string + { + return preg_replace_callback( + '~\r?\n(?=(.?))~', + fn (array $matches): string => $matches[1] === '' || $matches[1] === "\r" || $matches[1] === "\n" + ? $this->lineStart + : $this->lineStart . ' ', + $s, + ) ?? $s; + } + + private function detectLineStart(TokenIterator $originalTokens): string + { + foreach ($originalTokens->getTokens() as $token) { + if ($token[Lexer::TYPE_OFFSET] !== Lexer::TOKEN_PHPDOC_EOL) { + continue; + } + + $asteriskPosition = strpos($token[Lexer::VALUE_OFFSET], '*'); + if ($asteriskPosition === false) { + continue; + } + + return substr($token[Lexer::VALUE_OFFSET], 0, $asteriskPosition + 1); + } + + return ($originalTokens->getDetectedNewline() ?? "\n") . ' *'; + } + } diff --git a/upload/api/vendor/radebatz/type-info-extras/Readme.md b/upload/api/vendor/radebatz/type-info-extras/Readme.md index c6971b6c..ad4d8737 100644 --- a/upload/api/vendor/radebatz/type-info-extras/Readme.md +++ b/upload/api/vendor/radebatz/type-info-extras/Readme.md @@ -8,9 +8,8 @@ Library adding some extra features to the Symfony [Type Info](https://github.com Compatible with Symfony [type-info:7.3.8+](https://github.com/symfony/type-info/tree/7.3), -[type-info:7.4.1+](https://github.com/symfony/type-info/tree/7.4), -[type-info:8.0+](https://github.com/symfony/type-info/tree/8.0) -and [type-info:8.1+](https://github.com/symfony/type-info/tree/8.1). +[type-info:7.4.1+](https://github.com/symfony/type-info/tree/7.4) +and [type-info:8.0+](https://github.com/symfony/type-info), including the current development branch. Basic Usage ----------- diff --git a/upload/api/vendor/radebatz/type-info-extras/src/TypeResolver/StringTypeResolver.php b/upload/api/vendor/radebatz/type-info-extras/src/TypeResolver/StringTypeResolver.php index efcd3be0..41536128 100644 --- a/upload/api/vendor/radebatz/type-info-extras/src/TypeResolver/StringTypeResolver.php +++ b/upload/api/vendor/radebatz/type-info-extras/src/TypeResolver/StringTypeResolver.php @@ -36,6 +36,7 @@ use PHPStan\PhpDocParser\ParserConfig; use Radebatz\TypeInfoExtras\Type\Type; use Symfony\Component\TypeInfo\Exception\InvalidArgumentException; +use Symfony\Component\TypeInfo\Exception\LogicException; use Symfony\Component\TypeInfo\Exception\UnsupportedException; use Symfony\Component\TypeInfo\Type as BaseType; use Symfony\Component\TypeInfo\Type\BackedEnumType; @@ -63,6 +64,11 @@ final class StringTypeResolver implements TypeResolverInterface */ private static array $classExistCache = []; + /** + * @var array + */ + private array $resolvingExtraTypeAliases = []; + private readonly Lexer $lexer; private readonly TypeParser $parser; @@ -87,10 +93,7 @@ public function resolve(mixed $subject, ?TypeContext $typeContext = null): BaseT } try { - $tokens = new TokenIterator($this->lexer->tokenize($subject)); - $node = $this->parser->parse($tokens); - - return $this->getTypeFromNode($node, $typeContext); + return $this->getTypeFromNode($this->parse($subject), $typeContext); } catch (\DomainException $e) { throw new UnsupportedException(\sprintf('Cannot resolve "%s".', $subject), $subject, previous: $e); } @@ -198,62 +201,20 @@ private function getTypeFromNode(TypeNode $node, ?TypeContext $typeContext): Bas return CollectionType::mergeCollectionValueTypes($types); } - return match ($node->constExpr::class) { - ConstExprArrayNode::class => Type::array(), - ConstExprFalseNode::class => Type::false(), - ConstExprFloatNode::class => Type::float(), - ConstExprIntegerNode::class => Type::int(), - ConstExprNullNode::class => Type::null(), - ConstExprStringNode::class => Type::string(), - ConstExprTrueNode::class => Type::true(), + return match (true) { + $node->constExpr instanceof ConstExprArrayNode => Type::array(), + $node->constExpr instanceof ConstExprFalseNode => Type::false(), + $node->constExpr instanceof ConstExprFloatNode => Type::float(), + $node->constExpr instanceof ConstExprIntegerNode => Type::int(), + $node->constExpr instanceof ConstExprNullNode => Type::null(), + $node->constExpr instanceof ConstExprStringNode => Type::string(), + $node->constExpr instanceof ConstExprTrueNode => Type::true(), default => throw new \DomainException(\sprintf('Unhandled "%s" constant expression.', $node->constExpr::class)), }; } if ($node instanceof IdentifierTypeNode) { - $type = match ($node->name) { - 'bool', 'boolean' => Type::bool(), - 'true' => Type::true(), - 'false' => Type::false(), - 'int', 'integer' => Type::int(), - 'positive-int' => Type::intRange(from: 1, explicitType: $node->name), - 'negative-int' => Type::intRange(to: -1, explicitType: $node->name), - 'non-positive-int' => Type::intRange(to: 0, explicitType: $node->name), - 'non-negative-int' => Type::intRange(from: 0, explicitType: $node->name), - 'non-zero-int' => BaseType::union(Type::intRange(to: -1), Type::intRange(from: 1)), - 'float', 'double' => Type::float(), - 'string' => BaseType::string(), - 'class-string', - 'trait-string', - 'interface-string', - 'callable-string', - 'numeric-string', - 'lowercase-string', - 'non-empty-lowercase-string', - 'non-empty-string', - 'non-falsy-string', - 'truthy-string', - 'literal-string', - 'html-escaped-string' => Type::explicit(TypeIdentifier::STRING, $node->name), - 'resource' => Type::resource(), - 'object' => Type::object(), - 'callable' => Type::callable(), - 'array', 'non-empty-array' => Type::array(), - 'list', 'non-empty-list' => Type::list(), - 'iterable' => Type::iterable(), - 'mixed' => Type::mixed(), - 'null' => Type::null(), - 'array-key' => Type::arrayKey(), - 'scalar' => Type::union(Type::int(), Type::float(), Type::string(), Type::bool()), - 'number' => Type::union(Type::int(), Type::float()), - 'numeric' => Type::union(Type::int(), Type::float(), Type::string()), - 'self' => $typeContext ? Type::object($typeContext->getDeclaringClass()) : throw new InvalidArgumentException(\sprintf('A "%s" must be provided to resolve "self".', TypeContext::class)), - 'static' => $typeContext ? Type::object($typeContext->getCalledClass()) : throw new InvalidArgumentException(\sprintf('A "%s" must be provided to resolve "static".', TypeContext::class)), - 'parent' => $typeContext ? Type::object($typeContext->getParentClass()) : throw new InvalidArgumentException(\sprintf('A "%s" must be provided to resolve "parent".', TypeContext::class)), - 'void' => Type::void(), - 'never', 'never-return', 'never-returns', 'no-return' => Type::never(), - default => $this->resolveCustomIdentifier($node->name, $typeContext), - }; + $type = $this->resolveBuiltinIdentifier($node->name, $typeContext) ?? $this->resolveCustomIdentifier($node->name, $typeContext); if ($typeIsCollectionObject($type)) { return Type::collection($type); @@ -267,7 +228,17 @@ private function getTypeFromNode(TypeNode $node, ?TypeContext $typeContext): Bas } if ($node instanceof GenericTypeNode) { - if ($node->type instanceof IdentifierTypeNode && 'value-of' === $node->type->name) { + $type = null; + $keyword = $node->type instanceof IdentifierTypeNode ? $node->type->name : null; + if (null !== $keyword && ($lowercase = strtolower($keyword)) !== $keyword && \in_array($lowercase, ['value-of', 'key-of'], true)) { + try { + $type = $this->getTypeFromNode($node->type, $typeContext); + } catch (\DomainException) { + $keyword = $lowercase; + } + } + + if ('value-of' === $keyword) { $type = $this->getTypeFromNode($node->genericTypes[0], $typeContext); if ($type instanceof BackedEnumType) { return $type->getBackingType(); @@ -280,7 +251,7 @@ private function getTypeFromNode(TypeNode $node, ?TypeContext $typeContext): Bas throw new \DomainException(\sprintf('"%s" is not a valid type for "value-of".', $node->genericTypes[0])); } - if ($node->type instanceof IdentifierTypeNode && 'key-of' === $node->type->name) { + if ('key-of' === $keyword) { $type = $this->getTypeFromNode($node->genericTypes[0], $typeContext); if ($type instanceof CollectionType) { return $type->getCollectionKeyType(); @@ -289,7 +260,7 @@ private function getTypeFromNode(TypeNode $node, ?TypeContext $typeContext): Bas throw new \DomainException(\sprintf('"%s" is not a valid type for "key-of".', $node->genericTypes[0])); } - $type = $this->getTypeFromNode($node->type, $typeContext); + $type ??= $this->getTypeFromNode($node->type, $typeContext); // handle integer ranges if ($type->isIdentifiedBy(TypeIdentifier::INT)) { @@ -360,6 +331,53 @@ private function getTypeFromNode(TypeNode $node, ?TypeContext $typeContext): Bas throw new \DomainException(\sprintf('Unhandled "%s" node.', $node::class)); } + private function resolveBuiltinIdentifier(string $identifier, ?TypeContext $typeContext): ?BaseType + { + return match ($identifier) { + 'bool', 'boolean' => Type::bool(), + 'true' => Type::true(), + 'false' => Type::false(), + 'int', 'integer' => Type::int(), + 'positive-int' => Type::intRange(from: 1, explicitType: $identifier), + 'negative-int' => Type::intRange(to: -1, explicitType: $identifier), + 'non-positive-int' => Type::intRange(to: 0, explicitType: $identifier), + 'non-negative-int' => Type::intRange(from: 0, explicitType: $identifier), + 'non-zero-int' => BaseType::union(Type::intRange(to: -1), Type::intRange(from: 1)), + 'float', 'double' => Type::float(), + 'string' => BaseType::string(), + 'class-string', + 'trait-string', + 'interface-string', + 'callable-string', + 'numeric-string', + 'lowercase-string', + 'non-empty-lowercase-string', + 'non-empty-string', + 'non-falsy-string', + 'truthy-string', + 'literal-string', + 'html-escaped-string' => Type::explicit(TypeIdentifier::STRING, $identifier), + 'resource' => Type::resource(), + 'object' => Type::object(), + 'callable' => Type::callable(), + 'array', 'non-empty-array' => Type::array(), + 'list', 'non-empty-list' => Type::list(), + 'iterable' => Type::iterable(), + 'mixed' => Type::mixed(), + 'null' => Type::null(), + 'array-key' => Type::arrayKey(), + 'scalar' => Type::union(Type::int(), Type::float(), Type::string(), Type::bool()), + 'number' => Type::union(Type::int(), Type::float()), + 'numeric' => Type::union(Type::int(), Type::float(), Type::string()), + 'self' => $typeContext ? Type::object($typeContext->getDeclaringClass()) : throw new InvalidArgumentException(\sprintf('A "%s" must be provided to resolve "self".', TypeContext::class)), + 'static' => $typeContext ? Type::object($typeContext->getCalledClass()) : throw new InvalidArgumentException(\sprintf('A "%s" must be provided to resolve "static".', TypeContext::class)), + 'parent' => $typeContext ? Type::object($typeContext->getParentClass()) : throw new InvalidArgumentException(\sprintf('A "%s" must be provided to resolve "parent".', TypeContext::class)), + 'void' => Type::void(), + 'never', 'never-return', 'never-returns', 'no-return' => Type::never(), + default => null, + }; + } + private function resolveCustomIdentifier(string $identifier, ?TypeContext $typeContext): BaseType { $className = $typeContext ? $typeContext->normalize($identifier) : $identifier; @@ -395,9 +413,46 @@ private function resolveCustomIdentifier(string $identifier, ?TypeContext $typeC } if (isset($this->extraTypeAliases[$identifier])) { - return $this->resolve($this->extraTypeAliases[$identifier]); + if (isset($this->resolvingExtraTypeAliases[$identifier])) { + throw new LogicException(\sprintf('Cannot resolve "%s" type alias as it is recursive.', $identifier)); + } + + $this->resolvingExtraTypeAliases[$identifier] = true; + + try { + return $this->resolve($this->extraTypeAliases[$identifier]); + } finally { + unset($this->resolvingExtraTypeAliases[$identifier]); + } + } + + if (($keyword = strtolower($identifier)) !== $identifier && null !== $type = $this->resolveBuiltinIdentifier($keyword, $typeContext)) { + return $type; } throw new \DomainException(\sprintf('Unhandled "%s" identifier.', $identifier)); } + + private function parse(string $subject): TypeNode + { + $lexerTokens = $this->lexer->tokenize($subject); + $tokens = new TokenIterator($lexerTokens); + $node = $this->parser->parse($tokens); + + while ($tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL)) { + } + + $i = $tokens->currentTokenIndex(); + if (Lexer::TOKEN_OPEN_CURLY_BRACKET === ($lexerTokens[$i][Lexer::TYPE_OFFSET] ?? null)) { + while (\in_array($lexerTokens[--$i][Lexer::TYPE_OFFSET] ?? null, [Lexer::TOKEN_HORIZONTAL_WS, Lexer::TOKEN_PHPDOC_EOL], true)) { + } + + $value = $lexerTokens[$i][Lexer::VALUE_OFFSET] ?? ''; + if (($keyword = strtolower($value)) !== $value && \in_array($keyword, ['array', 'list', 'non-empty-array', 'non-empty-list'], true)) { + throw new \DomainException(\sprintf('Use "%s{...}" instead of "%s{...}" for shape types.', $keyword, $value)); + } + } + + return $node; + } } diff --git a/upload/api/vendor/symfony/console/Application.php b/upload/api/vendor/symfony/console/Application.php index 886608e4..41ddc95b 100644 --- a/upload/api/vendor/symfony/console/Application.php +++ b/upload/api/vendor/symfony/console/Application.php @@ -52,6 +52,7 @@ use Symfony\Component\Console\SignalRegistry\SignalRegistry; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\ErrorHandler\ErrorHandler; +use Symfony\Component\EventDispatcher\EventDispatcherInterface as ComponentEventDispatcherInterface; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use Symfony\Contracts\Service\ResetInterface; @@ -1035,9 +1036,18 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI $this->getSignalRegistry()->pushCurrentHandlers(); if ($this->dispatcher) { + // Build the lazy listeners before a signal handler needs them: a signal can interrupt the autoloading of a class they use, which cannot be loaded again from the handler + if ($this->signalsToDispatchEvent && $this->dispatcher instanceof ComponentEventDispatcherInterface) { + foreach ([ConsoleEvents::SIGNAL, ConsoleAlarmEvent::class, ConsoleEvents::TERMINATE] as $eventName) { + $this->dispatcher->getListeners($eventName); + } + } + // We register application signals, so that we can dispatch the event foreach ($this->signalsToDispatchEvent as $signal) { - $signalEvent = new ConsoleSignalEvent($command, $input, $output, $signal); + // When this application runs inside another application that handles the signal, the outer application decides if the process exits + $previousHandler = pcntl_signal_get_handler($signal); + $signalEvent = new ConsoleSignalEvent($command, $input, $output, $signal, \is_array($previousHandler) && $previousHandler[0] instanceof SignalRegistry ? false : 0); $alarmEvent = \SIGALRM === $signal ? new ConsoleAlarmEvent($command, $input, $output) : null; $signalRegistry->register($signal, function ($signal) use ($signalEvent, $alarmEvent, $command, $commandSignals, $input, $output) { diff --git a/upload/api/vendor/symfony/console/Command/CompleteCommand.php b/upload/api/vendor/symfony/console/Command/CompleteCommand.php index 44d23cc6..bdc3a8c5 100644 --- a/upload/api/vendor/symfony/console/Command/CompleteCommand.php +++ b/upload/api/vendor/symfony/console/Command/CompleteCommand.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Console\Command; +use Symfony\Component\Console\Application; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; @@ -20,6 +21,7 @@ use Symfony\Component\Console\Completion\Output\ZshCompletionOutput; use Symfony\Component\Console\Exception\CommandNotFoundException; use Symfony\Component\Console\Exception\ExceptionInterface; +use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; @@ -35,6 +37,7 @@ final class CompleteCommand extends Command public const COMPLETION_API_VERSION = '1'; private array $completionOutputs; + private ?InputDefinition $fullDefinition = null; private bool $isDebug = false; /** @@ -52,6 +55,53 @@ public function __construct(array $completionOutputs = []) parent::__construct(); } + public function setApplication(?Application $application): void + { + $this->fullDefinition = null; + + parent::setApplication($application); + } + + public function setDefinition(array|InputDefinition $definition): static + { + $this->fullDefinition = null; + + return parent::setDefinition($definition); + } + + /** + * @internal + */ + public function mergeApplicationDefinition(bool $mergeArgs = true): void + { + if (!$application = $this->getApplication()) { + return; + } + + $definition = $this->getNativeDefinition(); + $this->fullDefinition = new InputDefinition(); + $this->fullDefinition->setOptions($definition->getOptions()); + + foreach ($application->getDefinition()->getOptions() as $option) { + // the completion scripts rely on the options of this command, they win over the conflicting application ones + if ($definition->hasOption($option->getName()) || array_filter(explode('|', $option->getShortcut() ?? ''), $definition->hasShortcut(...))) { + continue; + } + + $this->fullDefinition->addOption($option); + } + + if ($mergeArgs) { + $this->fullDefinition->setArguments($application->getDefinition()->getArguments()); + } + $this->fullDefinition->addArguments($definition->getArguments()); + } + + public function getDefinition(): InputDefinition + { + return $this->fullDefinition ?? parent::getDefinition(); + } + protected function configure(): void { $this diff --git a/upload/api/vendor/symfony/console/Command/InvokableCommand.php b/upload/api/vendor/symfony/console/Command/InvokableCommand.php index 016da5a0..80808357 100644 --- a/upload/api/vendor/symfony/console/Command/InvokableCommand.php +++ b/upload/api/vendor/symfony/console/Command/InvokableCommand.php @@ -35,6 +35,11 @@ */ class InvokableCommand implements SignalableCommandInterface { + /** + * @var list + */ + private static array $running = []; + private readonly ?SignalableCommandInterface $signalableCommand; private readonly \ReflectionFunction $invokable; /** @@ -58,7 +63,13 @@ public function __construct( */ public function __invoke(InputInterface $input, OutputInterface $output): int { - $statusCode = $this->invokable->invoke(...$this->getParameters($this->invokable, $input, $output)); + self::$running[] = [$this->invokable->getClosureThis(), $this->command]; + + try { + $statusCode = $this->invokable->invoke(...$this->getParameters($this->invokable, $input, $output)); + } finally { + array_pop(self::$running); + } if (!\is_int($statusCode)) { if ($this->triggerDeprecations) { @@ -114,6 +125,20 @@ public function getCode(): callable return $this->code; } + /** + * Returns the command that is currently running the given object, if any. + */ + public static function getRunningCommand(object $code): ?Command + { + for ($i = \count(self::$running) - 1; 0 <= $i; --$i) { + if ($code === self::$running[$i][0]) { + return self::$running[$i][1]; + } + } + + return null; + } + private function getClosure(callable $code): \Closure { if (!$code instanceof \Closure) { diff --git a/upload/api/vendor/symfony/console/Command/LockableTrait.php b/upload/api/vendor/symfony/console/Command/LockableTrait.php index b7abd2fd..25261a75 100644 --- a/upload/api/vendor/symfony/console/Command/LockableTrait.php +++ b/upload/api/vendor/symfony/console/Command/LockableTrait.php @@ -55,20 +55,22 @@ private function lock(?string $name = null, bool $blocking = false): bool if (!$name) { if ($this instanceof Command) { $name = $this->getName(); + } elseif ($command = InvokableCommand::getRunningCommand($this)) { + $name = $command->getName(); } elseif ($attribute = (new \ReflectionClass($this::class))->getAttributes(AsCommand::class)) { - $name = $attribute[0]->newInstance()->name; + $name = explode('|', ltrim($attribute[0]->newInstance()->name, '|'), 2)[0]; } else { throw new LogicException(\sprintf('Lock name missing: provide it via "%s()", #[AsCommand] attribute, or by extending Command class.', __METHOD__)); } } - $this->lock = $this->lockFactory->createLock($name); - if (!$this->lock->acquire($blocking)) { - $this->lock = null; - + $lock = $this->lockFactory->createLock($name); + if (!$lock->acquire($blocking)) { return false; } + $this->lock = $lock; + return true; } @@ -77,9 +79,9 @@ private function lock(?string $name = null, bool $blocking = false): bool */ private function release(): void { - if ($this->lock) { - $this->lock->release(); + if ($lock = $this->lock) { $this->lock = null; + $lock->release(); } } } diff --git a/upload/api/vendor/symfony/console/DependencyInjection/AddConsoleCommandPass.php b/upload/api/vendor/symfony/console/DependencyInjection/AddConsoleCommandPass.php index 3198f26e..9fef057d 100644 --- a/upload/api/vendor/symfony/console/DependencyInjection/AddConsoleCommandPass.php +++ b/upload/api/vendor/symfony/console/DependencyInjection/AddConsoleCommandPass.php @@ -112,6 +112,11 @@ public function process(ContainerBuilder $container): void $usages ??= $tag['usages'] ?? null; } + if ($invokableRef) { + $help ??= $attribute?->help; + $usages ??= $attribute?->usages; + } + $definition->addMethodCall('setName', [$commandName]); if ($aliases) { diff --git a/upload/api/vendor/symfony/console/Formatter/OutputFormatter.php b/upload/api/vendor/symfony/console/Formatter/OutputFormatter.php index 1e757183..21800ebe 100644 --- a/upload/api/vendor/symfony/console/Formatter/OutputFormatter.php +++ b/upload/api/vendor/symfony/console/Formatter/OutputFormatter.php @@ -24,8 +24,12 @@ */ class OutputFormatter implements WrappableOutputFormatterInterface { + private const ANSI_PATTERN = '\\x1B(?:\\[[0-9;:]*m|\\]8;[^;]*;[^\\x1B]*\\x1B\\\\)'; + private const ANSI_REGEX = '/'.self::ANSI_PATTERN.'/'; + private array $styles = []; private OutputFormatterStyleStack $styleStack; + private array $charWidths = []; public function __clone() { @@ -128,6 +132,7 @@ public function formatAndWrap(?string $message, int $width): string $openTagRegex = '[a-z](?:[^\\\\<>]*+ | \\\\.)*'; $closeTagRegex = '[a-z][^<>]*+'; $currentLineLength = 0; + $lineBreak = false; preg_match_all("#<(($openTagRegex) | /($closeTagRegex)?)>#ix", $message, $matches, \PREG_OFFSET_CAPTURE); foreach ($matches[0] as $i => $match) { $pos = $match[1]; @@ -139,7 +144,7 @@ public function formatAndWrap(?string $message, int $width): string // tags are delimited by "<" and ">", which are ASCII, so a tag offset can never // fall inside a multibyte sequence: slicing by bytes is safe here - $output .= $this->applyCurrentStyle(substr($message, $offset, $pos - $offset), $output, $width, $currentLineLength); + $output .= $this->applyCurrentStyle(substr($message, $offset, $pos - $offset), $width, $currentLineLength, $lineBreak); $offset = $pos + \strlen($text); // opening tag? @@ -153,7 +158,7 @@ public function formatAndWrap(?string $message, int $width): string // $this->styleStack->pop(); } elseif (null === $style = $this->createStyleFromString($tag)) { - $output .= $this->applyCurrentStyle($text, $output, $width, $currentLineLength); + $output .= $this->applyCurrentStyle($text, $width, $currentLineLength, $lineBreak); } elseif ($open) { $this->styleStack->push($style); } else { @@ -161,9 +166,18 @@ public function formatAndWrap(?string $message, int $width): string } } - $output .= $this->applyCurrentStyle(substr($message, $offset), $output, $width, $currentLineLength); + $output .= $this->applyCurrentStyle(substr($message, $offset), $width, $currentLineLength, $lineBreak); + + if ($lineBreak) { + $output .= "\n"; + } + + if ($width && str_contains($output, "\r")) { + // keep "\r\n" line breaks together when ANSI codes end the line + $output = preg_replace('/\r((?:'.self::ANSI_PATTERN.')++)(?=\n|$)/', "\$1\r", $output); + } - return strtr($output, ["\0" => '\\', '\\<' => '<', '\\>' => '>']); + return str_replace("\0", '\\', $output); } public function getStyleStack(): OutputFormatterStyleStack @@ -176,8 +190,8 @@ public function getStyleStack(): OutputFormatterStyleStack */ private function createStyleFromString(string $string): ?OutputFormatterStyleInterface { - if (isset($this->styles[$string])) { - return $this->styles[$string]; + if (isset($this->styles[$lowerString = strtolower($string)])) { + return $this->styles[$lowerString]; } if (!preg_match_all('/([^=]+)=([^;]+)(;|$)/', $string, $matches, \PREG_SET_ORDER)) { @@ -213,58 +227,108 @@ private function createStyleFromString(string $string): ?OutputFormatterStyleInt /** * Applies current style from stack to text, if must be applied. */ - private function applyCurrentStyle(string $text, string $current, int $width, int &$currentLineLength): string + private function applyCurrentStyle(string $text, int $width, int &$currentLineLength, bool &$lineBreak): string { if ('' === $text) { return ''; } - if (!$width) { + if (!$width || str_contains($text, "\e") && '' === preg_replace(self::ANSI_REGEX, '', $text)) { + $text = str_replace(['\\<', '\\>'], ['<', '>'], $text); + return $this->isDecorated() ? $this->styleStack->getCurrent()->apply($text) : $text; } - if (!$currentLineLength && '' !== $current) { - $text = ltrim($text); + $text = str_replace(['\\<', '\\>'], ['<', '>'], $text); + $prefix = ''; + $visibleText = str_contains($text, "\e") ? preg_replace(self::ANSI_REGEX, '', $text) : $text; + + if ($currentLineLength >= $width) { + // the current line is full: drop the whitespace that follows and break the line before the next character + if (str_contains($text, "\e")) { + $text = preg_replace_callback('/^(?:[\\x00\\x09\\x0B\\x0D\\x20]|'.self::ANSI_PATTERN.')++/', static function ($match) { + preg_match_all(self::ANSI_REGEX, $match[0], $ansi); + + return implode('', $ansi[0]); + }, $text); + $visibleText = ltrim($visibleText, " \t\r\0\x0B"); + } else { + $text = $visibleText = ltrim($text, " \t\r\0\x0B"); + } + + // whitespace at the end of the message still breaks the line + if ($lineBreak = '' === $visibleText) { + return '' === $text || !$this->isDecorated() ? $text : $this->styleStack->getCurrent()->apply($text); + } + + if ("\n" !== $visibleText[0]) { + $prefix = "\n"; + } + + $currentLineLength = 0; } + $trailingNewlines = substr($visibleText, \strlen(rtrim($visibleText, "\r\n"))); + if ($currentLineLength) { $lines = explode("\n", $text, 2); - $prefix = Helper::substr($lines[0], 0, $i = $width - $currentLineLength)."\n"; - $text = Helper::substr($lines[0], $i); + $eol = "\n"; + if (isset($lines[1]) && str_ends_with($lines[0], "\r")) { + $lines[0] = substr($lines[0], 0, -1); + $eol = "\r\n"; + } + $i = $width - $currentLineLength; + if ($widths = $this->getWidths($lines[0])) { + $room = $i; + $i = \count($widths); + foreach ($widths as $j => $charWidth) { + if (0 > $room -= $charWidth) { + $i = $j; + break; + } + } + } + $prefix = ($i ? $this->substr($lines[0], 0, $i) : '')."\n"; + $text = $i ? $this->substr($lines[0], $i) : $lines[0]; if (isset($lines[1])) { // $prefix may contain the full first line in which the \n is already a part of $prefix. if ('' !== $text) { - $text .= "\n"; + $text .= $eol; + } else { + $prefix = substr($prefix, 0, -1).$eol; } $text .= $lines[1]; } - } else { - $prefix = ''; } - preg_match('~(\\n)$~', $text, $matches); $text = $prefix.$this->addLineBreaks($text, $width); - $text = rtrim($text, "\n").($matches[1] ?? ''); - - if (!$currentLineLength && '' !== $current && !str_ends_with($current, "\n")) { - $text = "\n".$text; + if (str_contains($text, "\e")) { + $parts = preg_split('/('.self::ANSI_PATTERN.')/', $text, -1, \PREG_SPLIT_DELIM_CAPTURE); + $sequences = ''; + for ($i = \count($parts) - 1; $i >= 0; --$i) { + if ($i % 2) { + $sequences = $parts[$i].$sequences; + } elseif ('' !== $parts[$i] = rtrim($parts[$i], "\r\n")) { + break; + } + } + $text = implode('', \array_slice($parts, 0, $i + 1)).$sequences; + } else { + $text = rtrim($text, "\r\n"); } + $text .= $trailingNewlines; $lines = explode("\n", $text); foreach ($lines as $i => $line) { - $currentLineLength = 0 === $i ? $currentLineLength + Helper::length($line) : Helper::length($line); - if ($width <= $currentLineLength) { - $currentLineLength = 0; - } + $lineLength = array_sum($this->getWidths($line)) ?: Helper::length(str_contains($line, "\e") ? preg_replace(self::ANSI_REGEX, '', $line) : $line); + $currentLineLength = 0 === $i ? $currentLineLength + $lineLength : $lineLength; } - if ($this->isDecorated()) { - foreach ($lines as $i => $line) { - $lines[$i] = $this->styleStack->getCurrent()->apply($line); - } + foreach ($lines as $i => $line) { + $lines[$i] = $this->isDecorated() ? $this->styleStack->getCurrent()->apply($line) : $line; } return implode("\n", $lines); @@ -273,7 +337,156 @@ private function applyCurrentStyle(string $text, string $current, int $width, in private function addLineBreaks(string $text, int $width): string { $encoding = mb_detect_encoding($text, null, true) ?: 'UTF-8'; + $widths = $this->getWidths($text); + + if (!$widths && !str_contains($text, "\e") && !str_contains($text, "\r\n")) { + return b($text)->toUnicodeString($encoding)->wordwrap($width, "\n", true)->toByteString($encoding); + } + + $chars = []; + $mask = $pending = ''; + foreach ($this->getTokens($text) as $token) { + if (preg_match(self::ANSI_REGEX, $token)) { + $pending .= $token; + continue; + } + $chars[] = [$pending, $token]; + $pending = ''; + $mask .= "\n" === $token || "\r\n" === $token ? '#' : (' ' === $token ? ' ' : '?'); + } + + if (!$chars) { + return $pending; + } + + $wrapped = rtrim($mask, '#'); + $mask = ($widths ? $this->wordwrap($wrapped, $width, $widths) : wordwrap($wrapped, $width, '#', true)).substr($mask, \strlen($wrapped)); + $result = ''; + $j = 0; + foreach (str_split($mask) as $char) { + if ('#' === $char) { + $eol = "\n"; + if (isset($chars[$j]) && ("\n" === $chars[$j][1] || "\r\n" === $chars[$j][1] || ' ' === $chars[$j][1])) { + $eol = ' ' === $chars[$j][1] ? "\n" : $chars[$j][1]; + $result .= $chars[$j++][0]; + } + $result .= $eol; + } else { + $result .= implode('', $chars[$j++]); + } + } + + return $result.$pending; + } + + private function substr(string $text, int $from, ?int $length = null): string + { + if (!str_contains($text, "\e")) { + return Helper::substr($text, $from, $length); + } + + $result = ''; + $position = 0; + foreach ($this->getTokens($text) as $token) { + $isAnsi = preg_match(self::ANSI_REGEX, $token); + if ($position >= $from && (null === $length || $position < $from + $length || $isAnsi && $position === $from + $length) + && (!$isAnsi || !$from || $position > $from) + ) { + $result .= $token; + } + if (!$isAnsi) { + ++$position; + } + } + + return $result; + } + + private function getTokens(string $text): iterable + { + $encoding = mb_detect_encoding($text, null, true) ?: 'UTF-8'; + foreach (preg_split('/('.self::ANSI_PATTERN.')/', $text, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY) as $part) { + if (preg_match(self::ANSI_REGEX, $part)) { + yield $part; + continue; + } + foreach (b($part)->toUnicodeString($encoding)->chunk() as $char) { + yield (string) $char->toByteString($encoding); + } + } + } + + /** + * Returns the number of columns of each visible character, or an empty array when none uses more than one column. + * + * @return list + */ + private function getWidths(string $text): array + { + if (!preg_match('/[^\x00-\x{10FF}]/u', $text)) { + return []; + } + + $widths = []; + $wide = false; + foreach ($this->getTokens($text) as $token) { + if (!preg_match(self::ANSI_REGEX, $token)) { + $widths[] = $charWidth = 2 < \strlen($token) ? ($this->charWidths[$token] ??= Helper::width($token)) : 1; + $wide = $wide || 1 < $charWidth; + } + } + + return $wide ? $widths : []; + } + + /** + * Wraps a mask like wordwrap() does when cutting long words, but using the number of columns of each character. + * + * @param list $widths + */ + private function wordwrap(string $mask, int $width, array $widths): string + { + $result = ''; + $start = $lastSpace = $lineWidth = 0; + + for ($i = 0, $length = \strlen($mask); $i < $length; ++$i) { + if ('#' === $mask[$i]) { + $result .= substr($mask, $start, $i + 1 - $start); + $start = $lastSpace = $i + 1; + $lineWidth = 0; + + continue; + } + + if (' ' === $mask[$i]) { + if ($lineWidth >= $width) { + $result .= substr($mask, $start, $i - $start).'#'; + $start = $i + 1; + $lineWidth = 0; + } else { + ++$lineWidth; + } + $lastSpace = $i; + + continue; + } + + if ($lineWidth + $widths[$i] > $width && $start < $lastSpace) { + $result .= substr($mask, $start, $lastSpace - $start).'#'; + $start = $lastSpace = $lastSpace + 1; + $lineWidth = array_sum(\array_slice($widths, $start, $i - $start)); + } + + // cut long words, and also before a wide character that still doesn't fit after breaking at the last space + if ($lineWidth && $lineWidth + $widths[$i] > $width) { + $result .= substr($mask, $start, $i - $start).'#'; + $start = $lastSpace = $i; + $lineWidth = 0; + } + + $lineWidth += $widths[$i]; + } - return b($text)->toUnicodeString($encoding)->wordwrap($width, "\n", true)->toByteString($encoding); + return $result.substr($mask, $start); } } diff --git a/upload/api/vendor/symfony/console/Formatter/OutputFormatterStyle.php b/upload/api/vendor/symfony/console/Formatter/OutputFormatterStyle.php index 20a65b51..de800f98 100644 --- a/upload/api/vendor/symfony/console/Formatter/OutputFormatterStyle.php +++ b/upload/api/vendor/symfony/console/Formatter/OutputFormatterStyle.php @@ -40,12 +40,14 @@ public function __construct(?string $foreground = null, ?string $background = nu public function setForeground(?string $color): void { - $this->color = new Color($this->foreground = $color ?: '', $this->background, $this->options); + $this->color = new Color($foreground = $color ?: '', $this->background, $this->options); + $this->foreground = $foreground; } public function setBackground(?string $color): void { - $this->color = new Color($this->foreground, $this->background = $color ?: '', $this->options); + $this->color = new Color($this->foreground, $background = $color ?: '', $this->options); + $this->background = $background; } public function setHref(string $url): void @@ -55,23 +57,20 @@ public function setHref(string $url): void public function setOption(string $option): void { + $this->color = new Color($this->foreground, $this->background, [...$this->options, $option]); $this->options[] = $option; - $this->color = new Color($this->foreground, $this->background, $this->options); } public function unsetOption(string $option): void { - $pos = array_search($option, $this->options); - if (false !== $pos) { - unset($this->options[$pos]); - } - + $this->options = array_diff($this->options, [$option]); $this->color = new Color($this->foreground, $this->background, $this->options); } public function setOptions(array $options): void { - $this->color = new Color($this->foreground, $this->background, $this->options = $options); + $this->color = new Color($this->foreground, $this->background, $options); + $this->options = $options; } public function apply(string $text): string diff --git a/upload/api/vendor/symfony/console/Helper/ProgressBar.php b/upload/api/vendor/symfony/console/Helper/ProgressBar.php index 0ce49864..3982a3a4 100644 --- a/upload/api/vendor/symfony/console/Helper/ProgressBar.php +++ b/upload/api/vendor/symfony/console/Helper/ProgressBar.php @@ -521,13 +521,6 @@ private function overwrite(string $message): void --$lineCount; } - foreach ($messageLines as $messageLine) { - $messageLineLength = Helper::width(Helper::removeDecoration($this->output->getFormatter(), $messageLine)); - if ($messageLineLength > $this->terminal->getWidth()) { - $lineCount += floor($messageLineLength / $this->terminal->getWidth()); - } - } - $this->output->clear($lineCount); } else { $lineCount = substr_count($this->previousMessage, "\n"); diff --git a/upload/api/vendor/symfony/console/Helper/Table.php b/upload/api/vendor/symfony/console/Helper/Table.php index 80d7ed6e..aef14b98 100644 --- a/upload/api/vendor/symfony/console/Helper/Table.php +++ b/upload/api/vendor/symfony/console/Helper/Table.php @@ -15,6 +15,7 @@ use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Formatter\WrappableOutputFormatterInterface; +use Symfony\Component\Console\Output\BufferedOutput; use Symfony\Component\Console\Output\ConsoleSectionOutput; use Symfony\Component\Console\Output\OutputInterface; @@ -50,7 +51,9 @@ class Table private array $columnWidths = []; private array $configuredColumnWidths = []; private array $columnMaxWidths = []; - private bool $rendered = false; + private array $renderedLines = []; + private array $tagStyles = []; + private ?\WeakMap $rowspanLines = null; private string $displayOrientation = self::DISPLAY_ORIENTATION_DEFAULT; private static array $styles; @@ -235,16 +238,40 @@ public function addRow(TableSeparator|array $row): static */ public function appendRow(TableSeparator|array $row): static { - if (!$this->output instanceof ConsoleSectionOutput) { + $output = $this->output; + if (!$output instanceof ConsoleSectionOutput) { throw new RuntimeException(\sprintf('Output should be an instance of "%s" when calling "%s".', ConsoleSectionOutput::class, __METHOD__)); } - if ($this->rendered) { - $this->output->clear($this->calculateRowCount()); + if (!$output->isDecorated()) { + $this->addRow($row); + $this->render(); + + return $this; + } + + $previous = $this->renderedLines; + if ($previous && !str_ends_with($output->getContent(), implode('', $previous))) { + // the section was cleared or written to since the table was rendered: keep its content and render the whole table again + $previous = []; } $this->addRow($row); - $this->render(); + $lines = $this->renderLines(); + + // Only rewrite the lines after the part both renderings have in common. + // While no column gets wider, that is just the bottom border, so lines that scrolled out of the terminal, which can't be cleared, are not touched. + $common = 0; + $max = min(\count($previous), \count($lines)); + while ($common < $max && $previous[$common] === $lines[$common]) { + ++$common; + } + + if (\count($previous) > $common) { + $output->clear(\count($previous) - $common); + } + $output->write(implode('', \array_slice($lines, $common)), false, OutputInterface::OUTPUT_RAW); + $this->renderedLines = $lines; return $this; } @@ -314,6 +341,15 @@ public function setVertical(bool $vertical = true): static */ public function render(): void { + if ($this->output instanceof ConsoleSectionOutput && $this->output->isDecorated()) { + // remember the lines on screen, so that appendRow() can rewrite only the ones that change + $lines = $this->renderLines(); + $this->output->write(implode('', $lines), false, OutputInterface::OUTPUT_RAW); + $this->renderedLines = $lines; + + return; + } + $divider = new TableSeparator(); $isCellWithColspan = static fn ($cell) => $cell instanceof TableCell && $cell->getColspan() >= 2; @@ -323,13 +359,13 @@ public function render(): void $rows = []; if ($horizontal) { foreach ($this->headers[0] ?? [] as $i => $header) { - $rows[$i] = [$header]; + $rows[$i] = [$this->closeStyles($header)]; foreach ($this->rows as $row) { if ($row instanceof TableSeparator) { continue; } if (isset($row[$i])) { - $rows[$i][] = $row[$i]; + $rows[$i][] = $this->closeStyles($row[$i]); } elseif ($isCellWithColspan($rows[$i][0])) { // Noop, there is a "title" } else { @@ -339,7 +375,8 @@ public function render(): void } } elseif ($vertical) { $formatter = $this->output->getFormatter(); - $maxHeaderLength = array_reduce($this->headers[0] ?? [], static fn ($max, $header) => max($max, Helper::width(Helper::removeDecoration($formatter, $header))), 0); + $headers = array_map($this->closeStyles(...), $this->headers[0] ?? []); + $maxHeaderLength = array_reduce($headers, static fn ($max, $header) => max($max, Helper::width(Helper::removeDecoration($formatter, $header))), 0); foreach ($this->rows as $row) { if ($row instanceof TableSeparator) { @@ -357,13 +394,12 @@ public function render(): void } } - $headers = $this->headers[0] ?? []; $maxRows = max(\count($headers), \count($row)); for ($i = 0; $i < $maxRows; ++$i) { - $cell = (string) ($row[$i] ?? ''); + $cell = (string) $this->closeStyles($row[$i] ?? ''); $eol = str_contains($cell, "\r\n") ? "\r\n" : "\n"; - $parts = explode($eol, $cell); + $parts = explode($eol, $this->closeStyles($cell, $eol)); foreach ($parts as $idx => $part) { if ($headers && !$containsColspan) { if (0 === $idx) { @@ -387,7 +423,8 @@ public function render(): void } } } else { - $rows = array_merge($this->headers, [$divider], $this->rows); + $closeStyles = $this->closeStyles(...); + $rows = array_map(static fn ($row) => $row instanceof TableSeparator ? $row : array_map($closeStyles, $row), array_merge($this->headers, [$divider], $this->rows)); } $this->calculateNumberOfColumns($rows); @@ -395,9 +432,13 @@ public function render(): void $rowGroups = $this->buildTableRows($rows); $this->calculateColumnsWidth($rowGroups); + $isMarkdown = self::$styles['markdown'] === $this->style; + // a markdown table must start with a header row, so one is added when the table has none + $needsMarkdownHeader = $isMarkdown && ($horizontal || $vertical || !array_filter($this->headers)); $isHeader = !$horizontal; $isFirstRow = $horizontal; - $hasTitle = (bool) $this->headerTitle; + // a title would break the delimiter row of a markdown table + $hasTitle = (bool) $this->headerTitle && !$isMarkdown; $previousRow = null; $pendingSeparators = 0; @@ -407,7 +448,8 @@ public function render(): void foreach ($rowGroup as $row) { if ($divider === $row) { $isHeader = false; - $isFirstRow = true; + // the delimiter row of a markdown table is written after its first header row + $isFirstRow = !$isMarkdown; continue; } @@ -423,6 +465,13 @@ public function render(): void continue; } + if ($needsMarkdownHeader) { + $this->renderRow(array_fill(0, $this->numberOfColumns, ''), $this->style->getCellHeaderFormat()); + $this->renderRowSeparator(self::SEPARATOR_TOP_BOTTOM); + $needsMarkdownHeader = false; + $isFirstRow = false; + } + for (; $pendingSeparators > 0; --$pendingSeparators) { $this->renderRowSeparator(self::SEPARATOR_MID, null, null, $previousRow, $row); } @@ -462,6 +511,12 @@ public function render(): void $this->renderRow($row, $isHeader ? $this->style->getCellHeaderFormat() : $this->style->getCellRowFormat()); } + if ($isMarkdown && $isHeader) { + // a markdown table has a single header row, so the next header rows are rendered as body rows + $this->renderRowSeparator(self::SEPARATOR_TOP_BOTTOM); + $isHeader = false; + } + $previousRow = $row; } } @@ -475,7 +530,6 @@ public function render(): void } $this->cleanup(); - $this->rendered = true; } /** @@ -604,34 +658,25 @@ private function renderCell(array $row, int $column, string $cellFormat): string } // str_pad won't work properly with multi-byte strings, we need to fix the padding - $width += \strlen($cell) - Helper::width($cell) - substr_count($cell, "\0"); + $width += \strlen($cell) - Helper::width(Helper::removeDecoration($this->output->getFormatter(), $cell)); $style = $this->getColumnStyle($column); if ($cell instanceof TableSeparator) { return \sprintf($style->getBorderFormat(), str_repeat($style->getBorderChars()[2], $width)); } - $width += Helper::length($cell) - Helper::length(Helper::removeDecoration($this->output->getFormatter(), $cell)); $content = \sprintf($style->getCellRowContentFormat(), $cell); $padType = $style->getPadType(); if ($cell instanceof TableCell && $cell->getStyle() instanceof TableCellStyle) { - $isNotStyledByTag = !preg_match('/^<(\w+|(\w+=[\w,]+;?)*)>.+<\/(\w+|(\w+=\w+;?)*)?>$/', $cell); + // the lines of a cell that spans rows get the style of the cell, even when a tag wraps them + $isNotStyledByTag = isset($this->rowspanLines[$cell]) || !preg_match('/^<(\w+|(\w+=[\w,]+;?)*)>.+<\/(\w+|(\w+=\w+;?)*)?>$/', $cell); if ($isNotStyledByTag) { $cellFormat = $cell->getStyle()->getCellFormat(); if (!\is_string($cellFormat)) { $tag = http_build_query($cell->getStyle()->getTagOptions(), '', ';'); $cellFormat = '<'.$tag.'>%s'; } - - if (str_contains($content, '')) { - $content = str_replace('', '', $content); - $width -= 3; - } - if (str_contains($content, '')) { - $content = str_replace('', '', $content); - $width -= \strlen(''); - } } $padType = $cell->getStyle()->getPadByAlign(); @@ -661,6 +706,7 @@ private function buildTableRows(array $rows): TableRows { /** @var WrappableOutputFormatterInterface $formatter */ $formatter = $this->output->getFormatter(); + $isMarkdown = self::$styles['markdown'] === $this->style; $unmergedRows = []; for ($rowKey = 0; $rowKey < \count($rows); ++$rowKey) { $rows = $this->fillNextRows($rows, $rowKey); @@ -690,8 +736,9 @@ private function buildTableRows(array $rows): TableRows } $cellWidth = Helper::width(Helper::removeDecoration($formatter, $cell)); - if ($minWrappedWidth && $cellWidth > $minWrappedWidth) { - $cell = $formatter->formatAndWrap($cell, $minWrappedWidth); + if ($wrapped = $minWrappedWidth && $cellWidth > $minWrappedWidth) { + // the wrapped cell is already formatted and gets formatted again when rendered + $cell = str_replace(['<', '>'], ['\\<', '\\>'], $formatter->formatAndWrap($cell, $minWrappedWidth)); } // update minimal columnWidths for spanned columns if ($colspan > 1 && $minWrappedWidth > 0) { @@ -717,13 +764,19 @@ private function buildTableRows(array $rows): TableRows $this->columnWidths[$i] = max($this->configuredColumnWidths[$i] ?? 0, $cellWidth + $lengthColumnBorder); } } - if (!str_contains($cell ?? '', "\n")) { + if ($isMarkdown && !$cell instanceof TableSeparator) { + // double the backslashes before a
so that markdown does not escape it, and add one that the output formatter removes + $escaped = preg_replace(['/\\\\++(?=[\r\n])/', '/\r\n?|\n/'], ['$0$0\\\\', '
'], str_replace('|', '\|', $cell ?? '')); + $rows[$rowKey][$column] = $cell instanceof TableCell || $colspan > 1 ? new TableCell($escaped, ['colspan' => $colspan, 'style' => $cell instanceof TableCell ? $cell->getStyle() : null]) : $escaped; + continue; } - $eol = str_contains($cell ?? '', "\r\n") ? "\r\n" : "\n"; - $escaped = implode($eol, array_map(OutputFormatter::escapeTrailingBackslash(...), explode($eol, $cell))); + if (!$wrapped && !str_contains($cell ?? '', "\n")) { + continue; + } + $escaped = implode("\n", array_map(OutputFormatter::escapeTrailingBackslash(...), preg_split('/\r?\n/', $wrapped ? $cell : $this->closeStyles(str_replace("\r\n", "\n", $cell), "\n")))); $cell = $cell instanceof TableCell ? new TableCell($escaped, ['colspan' => $cell->getColspan()]) : $escaped; - $lines = explode($eol, str_replace($eol, ''.$eol, $cell)); + $lines = explode("\n", str_replace("\n", ''."\n", $cell)); foreach ($lines as $lineKey => $line) { if ($colspan > 1) { $line = new TableCell($line, ['colspan' => $colspan]); @@ -754,21 +807,6 @@ private function buildTableRows(array $rows): TableRows }); } - private function calculateRowCount(): int - { - $numberOfRows = \count(iterator_to_array($this->buildTableRows(array_merge($this->headers, [new TableSeparator()], $this->rows)))); - - if ($this->headers) { - ++$numberOfRows; // Add row for header separator - } - - if ($this->rows) { - ++$numberOfRows; // Add row for footer separator - } - - return $numberOfRows; - } - /** * fill rows that contains rowspan > 1. * @@ -776,6 +814,7 @@ private function calculateRowCount(): int */ private function fillNextRows(array $rows, int $line): array { + $this->rowspanLines ??= new \WeakMap(); $unmergedRows = []; foreach ($rows[$line] as $column => $cell) { if (null !== $cell && !$cell instanceof TableCell && !\is_scalar($cell) && !$cell instanceof \Stringable) { @@ -786,10 +825,11 @@ private function fillNextRows(array $rows, int $line): array $lines = [$cell]; if (str_contains($cell, "\n")) { $eol = str_contains($cell, "\r\n") ? "\r\n" : "\n"; - $lines = explode($eol, str_replace($eol, ''.$eol.'', $cell)); + $lines = explode($eol, $this->closeStyles($cell, $eol)); $nbLines = \count($lines) > $nbLines ? substr_count($cell, $eol) : $nbLines; $rows[$line][$column] = new TableCell($lines[0], ['colspan' => $cell->getColspan(), 'style' => $cell->getStyle()]); + $this->rowspanLines[$rows[$line][$column]] = true; unset($lines[0]); } @@ -798,6 +838,7 @@ private function fillNextRows(array $rows, int $line): array foreach ($unmergedRows as $unmergedRowKey => $unmergedRow) { $value = $lines[$unmergedRowKey - $line] ?? ''; $unmergedRows[$unmergedRowKey][$column] = new TableCell($value, ['colspan' => $cell->getColspan(), 'style' => $cell->getStyle()]); + $this->rowspanLines[$unmergedRows[$unmergedRowKey][$column]] = true; if ($nbLines === $unmergedRowKey - $line) { break; } @@ -836,6 +877,10 @@ private function fillCells(iterable $row): iterable foreach ($row as $column => $cell) { $newRow[] = $cell; if ($cell instanceof TableCell && $cell->getColspan() > 1) { + if (self::$styles['markdown'] === $this->style) { + // markdown has no colspan and needs one cell per column, so the cell only fills the first column it spans + $newRow[array_key_last($newRow)] = new TableCell((string) $cell, ['style' => $cell->getStyle()]); + } foreach (range($column + 1, $column + $cell->getColspan() - 1) as $position) { // insert empty value at column position $newRow[] = ''; @@ -859,6 +904,77 @@ private function copyRow(array $rows, int $line): array return $row; } + /** + * Closes the styles that a cell leaves open at its end. + * + * With $eol, closes the styles that span a line break at the end of each line, and reopens them on the next line. + */ + private function closeStyles(mixed $cell, ?string $eol = null): mixed + { + if (!\is_string($cell) && !$cell instanceof \Stringable || !str_contains($value = (string) $cell, '<') || null !== $eol && !str_contains($value, $eol) || !($formatter = $this->output->getFormatter()) instanceof OutputFormatter) { + return $cell; + } + + $lines = null === $eol ? [$value] : explode($eol, $value); + // the tags that are open at the start of each line, then at the end of the last one, with the codes of their style and their closing tag + $openTags = [[]]; + foreach ($lines as $i => $line) { + $tags = $openTags[$i]; + preg_match_all('#<(?:[a-z](?:[^\\\\<>]*+|\\\\.)*|/(?:[a-z][^<>]*+)?)>#i', $line, $matches, \PREG_OFFSET_CAPTURE); + + foreach ($matches[0] as [$tag, $offset]) { + if ($offset && '\\' === $line[$offset - 1]) { + continue; + } + + if ('' !== $tag) { + // the formatter pushes the style of a style tag, and prints any other tag as is + $name = ltrim(substr($tag, 1), '/'); + $this->tagStyles[$name] ??= '' === $formatter->format('<'.$name) ? $formatter->getStyleStack()->pop()->apply('') : false; + if (false === $style = $this->tagStyles[$name]) { + continue; + } + + if ('/' !== $tag[1]) { + // a closing tag cannot contain an escaped "<" or ">" + $tags[] = [$tag, $style, strpbrk(substr($tag, 1, -1), '<>') ? '' : '' === $tag) { + array_pop($tags); + } else { + // like the formatter, close the last tag that has the same style, and the ones opened after it + for ($j = \count($tags) - 1; $j >= 0; --$j) { + if ($style === $tags[$j][1]) { + array_splice($tags, $j); + break; + } + } + } + } + + $openTags[] = $tags; + } + + if (!array_filter(\array_slice($openTags, 1, null === $eol ? null : -1))) { + return $cell; + } + + foreach ($lines as $i => $line) { + // closing tags go before the trailing backslashes of the line, which would escape them otherwise + $text = rtrim($line, '\\'); + $line = $text.implode('', array_reverse(array_column($openTags[$i + 1], 2))).substr($line, \strlen($text)); + $lines[$i] = implode('', array_column($openTags[$i], 0)).$line; + } + $value = implode($eol ?? '', $lines); + + return $cell instanceof TableCell ? new TableCell($value, ['rowspan' => $cell->getRowspan(), 'colspan' => $cell->getColspan(), 'style' => $cell->getStyle()]) : $value; + } + /** * Gets number of columns by row. */ @@ -908,7 +1024,7 @@ private function calculateColumnsWidth(iterable $groups): void if ($textLength > 0) { $contentColumns = mb_str_split($textContent, ceil($textLength / $cell->getColspan())); foreach ($contentColumns as $position => $content) { - $row[$i + $position] = $content; + $row[$i + $position] = OutputFormatter::escape($content); } } } @@ -939,7 +1055,7 @@ private function getCellWidth(array $row, int $column): int $columnWidth = $this->columnWidths[$column] ?? 0; $cellWidth = max($cellWidth, $columnWidth); - return isset($this->columnMaxWidths[$column]) ? min($this->columnMaxWidths[$column], $cellWidth) : $cellWidth; + return isset($this->columnMaxWidths[$column]) && self::$styles['markdown'] !== $this->style ? min($this->columnMaxWidths[$column], $cellWidth) : $cellWidth; } /** @@ -948,6 +1064,8 @@ private function getCellWidth(array $row, int $column): int private function cleanup(): void { $this->effectiveColumnWidths = []; + $this->tagStyles = []; + $this->rowspanLines = null; unset($this->numberOfColumns); } @@ -1016,4 +1134,22 @@ private function resolveStyle(TableStyle|string $name): TableStyle return self::$styles[$name] ?? throw new InvalidArgumentException(\sprintf('Style "%s" is not defined.', $name)); } + + /** + * @return string[] + */ + private function renderLines(): array + { + $output = $this->output; + $buffer = new BufferedOutput($output->getVerbosity(), true, $output->getFormatter()); + $this->output = $buffer; + + try { + $this->render(); + } finally { + $this->output = $output; + } + + return preg_split('/(?<=\n)/', $buffer->fetch(), -1, \PREG_SPLIT_NO_EMPTY) ?: []; + } } diff --git a/upload/api/vendor/symfony/console/Helper/TerminalInputHelper.php b/upload/api/vendor/symfony/console/Helper/TerminalInputHelper.php index d0bc120a..fe6d695e 100644 --- a/upload/api/vendor/symfony/console/Helper/TerminalInputHelper.php +++ b/upload/api/vendor/symfony/console/Helper/TerminalInputHelper.php @@ -102,7 +102,8 @@ public function finish(): void // and shell_exec() gives no way to detect that failure. Try the exact restore first so // any custom terminal settings survive, but always fall back to "stty sane" so the // terminal is left in a usable state even when the exact restore silently fails. - shell_exec('stty '.$this->initialState.' 2>/dev/null || stty sane'); + $nullDevice = '\\' === \DIRECTORY_SEPARATOR ? 'NUL' : '/dev/null'; + shell_exec('stty '.$this->initialState.' 2>'.$nullDevice.' || stty sane'); $this->signalToKill = 0; foreach ($this->signalHandlers as $signal => $originalHandler) { diff --git a/upload/api/vendor/symfony/console/Input/ArgvInput.php b/upload/api/vendor/symfony/console/Input/ArgvInput.php index 74f3fca2..b919667b 100644 --- a/upload/api/vendor/symfony/console/Input/ArgvInput.php +++ b/upload/api/vendor/symfony/console/Input/ArgvInput.php @@ -270,6 +270,13 @@ private function addLongOption(string $name, mixed $value): void } public function getFirstArgument(): ?string + { + $position = $this->getFirstArgumentPosition(); + + return null === $position ? null : $this->tokens[$position]; + } + + private function getFirstArgumentPosition(): ?int { $isOption = false; foreach ($this->tokens as $i => $token) { @@ -295,7 +302,7 @@ public function getFirstArgument(): ?string continue; } - return $token; + return $i; } return null; @@ -364,20 +371,9 @@ public function getRawTokens(bool $strip = false): array return $this->tokens; } - $parameters = []; - $keep = false; - foreach ($this->tokens as $value) { - if (!$keep && $value === $this->getFirstArgument()) { - $keep = true; - - continue; - } - if ($keep) { - $parameters[] = $value; - } - } + $position = $this->getFirstArgumentPosition(); - return $parameters; + return null === $position ? [] : \array_slice($this->tokens, $position + 1); } /** diff --git a/upload/api/vendor/symfony/console/Input/StringInput.php b/upload/api/vendor/symfony/console/Input/StringInput.php index a70f048f..1392bef2 100644 --- a/upload/api/vendor/symfony/console/Input/StringInput.php +++ b/upload/api/vendor/symfony/console/Input/StringInput.php @@ -63,7 +63,7 @@ private function tokenize(string $input): array $token = null; } } elseif (preg_match('/([^="\'\s]+?)(=?)('.self::REGEX_QUOTED_STRING.'+)/A', $input, $match, 0, $cursor)) { - $token .= $match[1].$match[2].stripcslashes(str_replace(['"\'', '\'"', '\'\'', '""'], '', substr($match[3], 1, -1))); + $token .= $match[1].$match[2].stripcslashes(preg_replace('/'.self::REGEX_QUOTED_STRING.'/', '$1$2', $match[3])); } elseif (preg_match('/'.self::REGEX_QUOTED_STRING.'/A', $input, $match, 0, $cursor)) { $token .= stripcslashes(substr($match[0], 1, -1)); } elseif (preg_match('/'.self::REGEX_UNQUOTED_STRING.'/A', $input, $match, 0, $cursor)) { diff --git a/upload/api/vendor/symfony/console/Output/ConsoleSectionOutput.php b/upload/api/vendor/symfony/console/Output/ConsoleSectionOutput.php index a45d177a..b86ca7f4 100644 --- a/upload/api/vendor/symfony/console/Output/ConsoleSectionOutput.php +++ b/upload/api/vendor/symfony/console/Output/ConsoleSectionOutput.php @@ -52,7 +52,8 @@ public function setMaxHeight(int $maxHeight): void $this->maxHeight = $maxHeight; $existingContent = $this->popStreamContentUntilCurrentSection($previousMaxHeight ? min($previousMaxHeight, $this->lines) : $this->lines); - parent::doWrite($this->getVisibleContent(), false); + $visibleContent = $this->getVisibleContent(); + parent::doWrite($visibleContent, '' !== $visibleContent && !str_ends_with($visibleContent, \PHP_EOL)); parent::doWrite($existingContent, false); } @@ -68,17 +69,31 @@ public function clear(?int $lines = null): void } if ($lines) { - array_splice($this->content, -$lines); + $width = $this->terminal->getWidth(); + foreach (array_splice($this->content, -$lines) as $line) { + $lines += $this->getDisplayHeight($line, $width) - 1; + } } else { $lines = $this->lines; $this->content = []; } + $previousLines = $this->lines; + // callers may ask to clear more lines than this section tracks (e.g. ProgressBar // counts "\n"-separated lines while addContent() splits on PHP_EOL), so keep the // counter from going negative, which would break the max-height bookkeeping $this->lines = max(0, $this->lines - $lines); + if ($this->maxHeight && $previousLines > $this->maxHeight) { + // the section was scrolled: redraw it to show the lines that are visible again + $erasedContent = $this->popStreamContentUntilCurrentSection($this->maxHeight); + parent::doWrite($this->getVisibleContent(), false); + parent::doWrite($erasedContent, false); + + return; + } + parent::doWrite($this->popStreamContentUntilCurrentSection($this->maxHeight ? min($this->maxHeight, $lines) : $lines), false); } @@ -157,7 +172,7 @@ public function addContent(string $input, bool $newline = true): int && !str_ends_with($lastLine, \PHP_EOL) ) { // deduct the line count of the previous line - $this->lines -= (int) ceil($this->getDisplayLength($lastLine) / $width) ?: 1; + $this->lines -= $this->getDisplayHeight($lastLine, $width); // concatenate previous and new line $lineContent = $lastLine.$lineContent; // replace last entry of `$this->content` with the new expanded line @@ -167,7 +182,7 @@ public function addContent(string $input, bool $newline = true): int $this->content[] = $lineContent; } - $linesAdded += (int) ceil($this->getDisplayLength($lineContent) / $width) ?: 1; + $linesAdded += $this->getDisplayHeight($lineContent, $width); } $this->lines += $linesAdded; @@ -198,25 +213,30 @@ protected function doWrite(string $message, bool $newline): void return; } + if ('' === $message && !$newline) { + return; + } + // Check if the previous line (last entry of `$this->content`) needs to be continued // (i.e. does not end with a line break). In which case, it needs to be erased first. - $linesToClear = $deleteLastLine = ($lastLine = end($this->content) ?: '') && !str_ends_with($lastLine, \PHP_EOL) ? 1 : 0; + $deleteLastLine = ($lastLine = end($this->content) ?: '') && !str_ends_with($lastLine, \PHP_EOL); + $linesToClear = $deleteLastLine ? $this->getDisplayHeight($lastLine, $this->terminal->getWidth()) : 0; + $previousLines = $this->lines; + + $this->addContent($message, $newline); - $linesAdded = $this->addContent($message, $newline); + if ($this->maxHeight > 0 && $this->lines > $this->maxHeight) { + // on overflow, clear the lines displayed so far and redraw the section (to remove the first lines) + $erasedContent = $this->popStreamContentUntilCurrentSection(min($this->maxHeight, $previousLines)); + $visibleContent = $this->getVisibleContent(); + parent::doWrite($visibleContent, !str_ends_with($visibleContent, \PHP_EOL)); + parent::doWrite($erasedContent, false); - if ($lineOverflow = $this->maxHeight > 0 && $this->lines > $this->maxHeight) { - // on overflow, clear the whole section and redraw again (to remove the first lines) - $linesToClear = $this->maxHeight; + return; } $erasedContent = $this->popStreamContentUntilCurrentSection($linesToClear); - if ($lineOverflow) { - // redraw existing lines of the section - $previousLinesOfSection = \array_slice($this->content, $this->lines - $this->maxHeight, $this->maxHeight - $linesAdded); - parent::doWrite(implode('', $previousLinesOfSection), false); - } - // if the last line was removed, re-print its content together with the new content. // otherwise, just print the new content. parent::doWrite($deleteLastLine ? $lastLine.$message : $message, true); @@ -256,8 +276,45 @@ private function popStreamContentUntilCurrentSection(int $numberOfLinesToClearFr return implode('', array_reverse($erasedContent)); } - private function getDisplayLength(string $text): int + private function getDisplayHeight(string $line, int $width): int { - return Helper::width(Helper::removeDecoration($this->getFormatter(), str_replace("\t", ' ', $text))); + if ($width <= 0) { + return 1; + } + + // the line break must not count as a column, or a line as wide as the terminal would count as two + if (str_ends_with($line, \PHP_EOL)) { + $line = substr($line, 0, -\strlen(\PHP_EOL)); + } + + // formatting the line again would remove literal tags, so only the ANSI sequences are removed + $line = preg_replace(['/\e\[[^m]*m/', '/\e]8;[^;]*;[^\e]*\e\\\\/'], '', $line); + $lineWidth = Helper::width(str_replace("\t", ' ', $line)); + + if ($lineWidth <= $width || !preg_match('/[\t\x80-\xFF]/', $line) || !preg_match('//u', $line)) { + return (int) ceil($lineWidth / $width) ?: 1; + } + + // a wide character that does not fit at the end of a row is moved to the next one + $rows = 1; + $column = 0; + $offset = 0; + $widths = []; + while (false !== $char = grapheme_extract($line, 1, \GRAPHEME_EXTR_COUNT, $offset, $offset)) { + if ("\t" === $char) { + // a tab moves the cursor to the next multiple of 8 columns, but never past the last column + $column = min($column - $column % 8 + 8, $width - 1); + continue; + } + + $charWidth = $widths[$char] ??= Helper::width($char); + if ($column + $charWidth > $width) { + ++$rows; + $column = 0; + } + $column += $charWidth; + } + + return $rows; } } diff --git a/upload/api/vendor/symfony/finder/Gitignore.php b/upload/api/vendor/symfony/finder/Gitignore.php index 77645889..97e4ddc6 100644 --- a/upload/api/vendor/symfony/finder/Gitignore.php +++ b/upload/api/vendor/symfony/finder/Gitignore.php @@ -46,7 +46,7 @@ private static function buildRegex(string $gitignoreFileContent, bool $inverted) continue; } - $line = preg_replace('~(? '['.('' !== $matches[1] ? '^' : '').str_replace('\\-', '-', $matches[2]).']', $regex); + $parts = preg_split('~\\\\(.)~s', $gitignoreLine, -1, \PREG_SPLIT_DELIM_CAPTURE); + + if (str_ends_with(end($parts), '\\')) { + return '$f'; // a trailing backslash never matches + } + + $regex = ''; + foreach ($parts as $i => $part) { + // escaped characters are written in hex so that the replacements below leave them alone + $regex .= $i % 2 ? \sprintf('\x%02X', \ord($part)) : preg_quote($part, '~'); + } + $regex = preg_replace_callback('~\\\\\[((?:\\\\[!^])?+)((?:\\\\\])?+[^\]]*)\\\\\]~', static fn (array $matches): string => ('' !== $matches[1] ? '(?!/)[^' : '[').self::rangesToRegex($matches[2]).']', $regex); $regex = preg_replace('~(?:(?:\\\\\*){2,}(/?))+~', '(?:(?:(?!//).(? 4 === \strlen($c) ? hexdec(substr($c, 2)) : \ord($c[-1]), [$m[1], $m[2]]); + + return $from <= $to ? $m[1].'-'.$m[2] : $m[1]; + }, $class) ?? $class; + } } diff --git a/upload/api/vendor/symfony/finder/Glob.php b/upload/api/vendor/symfony/finder/Glob.php index ac11e8e0..e0d2e245 100644 --- a/upload/api/vendor/symfony/finder/Glob.php +++ b/upload/api/vendor/symfony/finder/Glob.php @@ -42,7 +42,8 @@ public static function toRegex(string $glob, bool $strictLeadingDot = true, bool { $firstByte = true; $escaping = false; - $inCurlies = 0; + $curlies = []; + $bracketScanEnd = 0; $regex = ''; if ($unanchored = str_starts_with($glob, '**/')) { $glob = '/'.$glob; @@ -76,6 +77,8 @@ public static function toRegex(string $glob, bool $strictLeadingDot = true, bool if ($delimiter === $car || '.' === $car || '(' === $car || ')' === $car || '|' === $car || '+' === $car || '^' === $car || '$' === $car) { $regex .= "\\$car"; + } elseif ('[' === $car && !$escaping && null !== $class = self::bracketExpressionToRegex($glob, $i, $delimiter, $bracketScanEnd)) { + $regex .= $class; } elseif ('*' === $car) { if ($escaping || $strictWildcardSlash || !str_ends_with($regex, '.*')) { // ".*.*" matches the same as ".*" but backtracks quadratically @@ -84,16 +87,19 @@ public static function toRegex(string $glob, bool $strictLeadingDot = true, bool } elseif ('?' === $car) { $regex .= $escaping ? '\\?' : ($strictWildcardSlash ? '[^/]' : '.'); } elseif ('{' === $car) { - $regex .= $escaping ? '\\{' : '('; if (!$escaping) { - ++$inCurlies; + $curlies[] = [\strlen($regex)]; } - } elseif ('}' === $car && $inCurlies) { + $regex .= $escaping ? '\\{' : '('; + } elseif ('}' === $car && $curlies) { $regex .= $escaping ? '}' : ')'; if (!$escaping) { - --$inCurlies; + array_pop($curlies); + } + } elseif (',' === $car && $curlies) { + if (!$escaping) { + $curlies[\count($curlies) - 1][] = \strlen($regex); } - } elseif (',' === $car && $inCurlies) { $regex .= $escaping ? ',' : '|'; } elseif ('\\' === $car) { if ($escaping) { @@ -104,16 +110,76 @@ public static function toRegex(string $glob, bool $strictLeadingDot = true, bool } continue; + } elseif ($escaping && ('[' === $car || ']' === $car)) { + $regex .= '\\'.$car; } else { $regex .= $car; } $escaping = false; } + // A "{" that no "}" closes is a literal character, and so are the "," it contains + $prefix = ''; + $start = 0; + foreach (array_merge(...$curlies) as $offset) { + $prefix .= substr($regex, $start, $offset - $start).('(' === $regex[$offset] ? '\\{' : ','); + $start = $offset + 1; + } + $regex = $prefix.substr($regex, $start); + if ($unanchored) { $regex = substr_replace($regex, '?', 1 + ('/' === $delimiter) + ($strictLeadingDot ? \strlen('(?=[^\.])') : 0), 0); } return $delimiter.'^'.$regex.'$'.$delimiter; } + + /** + * Returns null when a "/" comes before the closing "]", and "\[" when no "]" closes the "[" at position $i. + * + * $scanEnd is where the last scan stopped without finding a "]": every "[" before that position gets the same result. + */ + private static function bracketExpressionToRegex(string $glob, int &$i, string $delimiter, int &$scanEnd): ?string + { + $sizeGlob = \strlen($glob); + + if ($i < $scanEnd) { + return $scanEnd < $sizeGlob ? null : '\\['; + } + + $class = '['; + + for ($j = $i + 1; $j < $sizeGlob; ++$j) { + $car = $glob[$j]; + + if (']' === $car && $j > $i + 1) { + $i = $j; + + return $class.']'; + } + + if ('/' === $car || ('\\' === $car && '/' === ($glob[$j + 1] ?? ''))) { + $scanEnd = $j; + + return null; + } + + if ('[' === $car && preg_match('/\[:[a-z]++:\]/A', $glob, $m, 0, $j)) { + $class .= $m[0]; + $j += \strlen($m[0]) - 1; + } elseif ('\\' === $car) { + if (!isset($glob[++$j])) { + break; + } + $class .= preg_quote($glob[$j], $delimiter); + } elseif ($delimiter === $car || str_contains('.()|+^$', $car)) { + $class .= '\\'.$car; + } else { + $class .= $car; + } + } + $scanEnd = $sizeGlob; + + return '\\['; + } } diff --git a/upload/api/vendor/symfony/finder/Iterator/MultiplePcreFilterIterator.php b/upload/api/vendor/symfony/finder/Iterator/MultiplePcreFilterIterator.php index 3450c49d..2a4c0b8a 100644 --- a/upload/api/vendor/symfony/finder/Iterator/MultiplePcreFilterIterator.php +++ b/upload/api/vendor/symfony/finder/Iterator/MultiplePcreFilterIterator.php @@ -87,12 +87,12 @@ protected function isRegex(string $str): bool $end = substr($m[1], -1); if ($start === $end) { - return !preg_match('/[*?[:alnum:] \\\\]/', $start); + return !preg_match('/[*?[:alnum:] \\\\]/', $start) && false !== @preg_match($str, ''); } foreach ([['{', '}'], ['(', ')'], ['[', ']'], ['<', '>']] as $delimiters) { if ($start === $delimiters[0] && $end === $delimiters[1]) { - return true; + return false !== @preg_match($str, ''); } } } diff --git a/upload/api/vendor/symfony/finder/Iterator/RecursiveDirectoryIterator.php b/upload/api/vendor/symfony/finder/Iterator/RecursiveDirectoryIterator.php index f5fd2d4d..c8702d5e 100644 --- a/upload/api/vendor/symfony/finder/Iterator/RecursiveDirectoryIterator.php +++ b/upload/api/vendor/symfony/finder/Iterator/RecursiveDirectoryIterator.php @@ -25,6 +25,7 @@ class RecursiveDirectoryIterator extends \RecursiveDirectoryIterator { private bool $ignoreUnreadableDirs; private bool $ignoreFirstRewind = true; + private array $realPaths = []; // these 3 properties take part of the performance optimization to avoid redoing the same work in all iterations private string $rootPath; @@ -46,6 +47,9 @@ public function __construct(string $path, int $flags, bool $ignoreUnreadableDirs if ('/' !== \DIRECTORY_SEPARATOR && !($flags & self::UNIX_PATHS)) { $this->directorySeparator = \DIRECTORY_SEPARATOR; } + if ($flags & self::FOLLOW_SYMLINKS && false !== ($realPath = realpath($path))) { + $this->realPaths[$realPath] = true; + } } /** @@ -76,6 +80,11 @@ public function hasChildren(bool $allowLinks = false): bool { $hasChildren = parent::hasChildren($allowLinks); + // don't loop on a directory that resolves to one of its parents + if ($hasChildren && $this->realPaths && false !== ($realPath = realpath($this->getPathname())) && isset($this->realPaths[$realPath])) { + return false; + } + if (!$hasChildren || !$this->ignoreUnreadableDirs) { return $hasChildren; } @@ -104,6 +113,8 @@ public function getChildren(): \RecursiveDirectoryIterator // performance optimization to avoid redoing the same work in all children $children->rootPath = $this->rootPath; + + $children->realPaths += $this->realPaths; } return $children; diff --git a/upload/api/vendor/symfony/finder/Iterator/VcsIgnoredFilterIterator.php b/upload/api/vendor/symfony/finder/Iterator/VcsIgnoredFilterIterator.php index 3715e269..cc5e45a5 100644 --- a/upload/api/vendor/symfony/finder/Iterator/VcsIgnoredFilterIterator.php +++ b/upload/api/vendor/symfony/finder/Iterator/VcsIgnoredFilterIterator.php @@ -18,7 +18,10 @@ */ final class VcsIgnoredFilterIterator extends \FilterIterator { + private string $searchDir; + private string $realSearchDir; private string $baseDir; + private ?string $gitInfoExcludeFile = null; /** * @var array|null> @@ -35,12 +38,14 @@ final class VcsIgnoredFilterIterator extends \FilterIterator */ public function __construct(\Iterator $iterator, string $baseDir) { - // paths are compared to the real path of each file, so the base directory needs the same treatment + $this->searchDir = rtrim($this->normalizePath($baseDir), '/').'/'; $this->baseDir = $this->normalizePath(realpath($baseDir) ?: $baseDir); + $this->realSearchDir = rtrim($this->baseDir, '/').'/'; foreach ([$this->baseDir, ...$this->parentDirectoriesUpwards($this->baseDir)] as $directory) { if (@file_exists("{$directory}/.git")) { $this->baseDir = $directory; + $this->gitInfoExcludeFile = $this->getGitInfoExcludePath(); break; } } @@ -50,21 +55,24 @@ public function __construct(\Iterator $iterator, string $baseDir) public function accept(): bool { - $file = $this->current(); + $path = $this->normalizePath($this->current()->getPathname()); - $fileRealPath = $this->normalizePath($file->getRealPath()); + // git matches a symlink by its own path, so only the search directory is resolved + if (str_starts_with($path, $this->searchDir)) { + $path = $this->realSearchDir.substr($path, \strlen($this->searchDir)); + } - return !$this->isIgnored($fileRealPath); + return !$this->isIgnored($path); } - private function isIgnored(string $fileRealPath): bool + private function isIgnored(string $path): bool { - if (isset($this->ignoredPathsCache[$fileRealPath])) { - return $this->ignoredPathsCache[$fileRealPath]; + if (isset($this->ignoredPathsCache[$path])) { + return $this->ignoredPathsCache[$path]; } $ignored = false; - $parentDirectories = $this->parentDirectoriesDownwards($fileRealPath); + $parentDirectories = $this->parentDirectoriesDownwards($path); if ($parentDirectories && $this->baseDir !== $parentDirectory = $parentDirectories[\count($parentDirectories) - 1]) { // paths inside an ignored directory are ignored and cannot be re-included @@ -72,15 +80,24 @@ private function isIgnored(string $fileRealPath): bool } if (!$ignored) { - $isDir = is_dir($fileRealPath); + $isDir = !is_link($path) && is_dir($path); + $gitignoreFiles = []; - // the last matching rule of the deepest .gitignore file decides foreach (array_reverse($parentDirectories) as $parentDirectory) { - if (null === $rules = $this->readGitignoreFile("{$parentDirectory}/.gitignore")) { + $gitignoreFiles["{$parentDirectory}/.gitignore"] = $parentDirectory; + } + + if (null !== $this->gitInfoExcludeFile && $this->baseDir === ($parentDirectories[0] ?? null)) { + $gitignoreFiles[$this->gitInfoExcludeFile] = $this->baseDir; + } + + // the last matching rule of the deepest .gitignore file decides, and the exclude file of the repository comes after the root one + foreach ($gitignoreFiles as $gitignoreFile => $parentDirectory) { + if (null === $rules = $this->readGitignoreFile($gitignoreFile)) { continue; } - $fileRelativePath = substr($fileRealPath, \strlen($parentDirectory) + 1); + $fileRelativePath = substr($path, \strlen($parentDirectory) + 1); foreach ($rules as [$regex, $isNegated, $isDirOnly]) { if ($isDirOnly && !$isDir) { @@ -96,7 +113,7 @@ private function isIgnored(string $fileRealPath): bool } } - return $this->ignoredPathsCache[$fileRealPath] = $ignored; + return $this->ignoredPathsCache[$path] = $ignored; } /** @@ -140,6 +157,32 @@ private function parentDirectoriesDownwards(string $fileRealPath): array ); } + private function getGitInfoExcludePath(): ?string + { + $gitDir = "{$this->baseDir}/.git"; + + // worktrees and submodules have a .git file that points to their git directory, which can be outside of open_basedir + if (!is_dir($gitDir)) { + if (!preg_match('~^gitdir:\s*(.+)~i', (string) @file_get_contents($gitDir), $matches)) { + return null; + } + + $gitDir = $this->resolveGitPath(trim($matches[1]), $this->baseDir); + + // the git directory of a worktree points to the common directory, where the exclude file is + if (false !== $commonDir = @file_get_contents("{$gitDir}/commondir")) { + $gitDir = $this->resolveGitPath(trim($commonDir), $gitDir); + } + } + + return @is_file("{$gitDir}/info/exclude") ? "{$gitDir}/info/exclude" : null; + } + + private function resolveGitPath(string $path, string $from): string + { + return preg_match('~^(?:[a-zA-Z]:)?[/\\\\]~', $path) ? $path : "{$from}/{$path}"; + } + /** * Returns the rules of a .gitignore file, last one first, as [regex, isNegated, isDirOnly] tuples. * diff --git a/upload/api/vendor/symfony/polyfill-intl-grapheme/Grapheme.php b/upload/api/vendor/symfony/polyfill-intl-grapheme/Grapheme.php index 94d080ac..a7e18d7d 100644 --- a/upload/api/vendor/symfony/polyfill-intl-grapheme/Grapheme.php +++ b/upload/api/vendor/symfony/polyfill-intl-grapheme/Grapheme.php @@ -49,6 +49,9 @@ final class Grapheme ['μ', 's', 'ι', 'σ', 'β', 'θ', 'φ', 'π', 'κ', 'ρ', 'ε', "\xE1\xB9\xA1", 'ι'], ]; + // The characters that extend a grapheme cluster and that ICU 74 ignores when it compares strings + private const IGNORABLE_RX = '/^[\x{034F}\x{0488}-\x{0489}\x{0591}-\x{05AF}\x{05BD}\x{05C4}-\x{05C5}\x{0610}-\x{061A}\x{06D6}-\x{06DC}\x{06DF}-\x{06E4}\x{06E7}-\x{06E8}\x{06EA}-\x{06ED}\x{0740}\x{0743}-\x{0744}\x{0747}-\x{074A}\x{0898}-\x{089D}\x{08CA}-\x{08E1}\x{08EA}-\x{08EF}\x{08F3}\x{0951}-\x{0952}\x{0F18}-\x{0F19}\x{0F35}\x{0F37}\x{0F3E}-\x{0F3F}\x{0F86}-\x{0F87}\x{0FC6}\x{17B4}-\x{17B5}\x{17D3}\x{180B}-\x{180D}\x{180F}\x{1A7F}\x{1B6B}-\x{1B73}\x{1CD0}-\x{1CD2}\x{1CD4}-\x{1CE8}\x{1CF4}\x{1CF7}-\x{1CF9}\x{200C}-\x{200D}\x{2D7F}\x{A670}-\x{A672}\x{A8E0}-\x{A8F1}\x{FE00}-\x{FE0F}\x{FE21}\x{FE23}-\x{FE26}\x{FE28}\x{FE2A}-\x{FE2D}\x{FE2F}\x{102E0}\x{10EFD}-\x{10EFF}\x{11366}-\x{1136C}\x{11370}-\x{11374}\x{13440}\x{13447}-\x{13455}\x{16FE4}\x{1CF00}-\x{1CF2D}\x{1CF30}-\x{1CF46}\x{1D165}-\x{1D169}\x{1D16D}-\x{1D172}\x{1D17B}-\x{1D182}\x{1D185}-\x{1D18B}\x{1D1AA}-\x{1D1AD}\x{1D242}-\x{1D244}\x{1DA00}-\x{1DA36}\x{1DA3B}-\x{1DA6C}\x{1DA75}\x{1DA84}\x{1DA9B}-\x{1DA9F}\x{1DAA1}-\x{1DAAF}\x{1E8D0}-\x{1E8D6}\x{E0020}-\x{E007F}\x{E0100}-\x{E01EF}]*+\z/u'; + // indexed by the $mode argument of grapheme_position() private const POSITION_FUNCTIONS = ['grapheme_strpos', 'grapheme_stripos', 'grapheme_strrpos', 'grapheme_strripos']; @@ -377,13 +380,72 @@ private static function grapheme_position($s, $needle, $offset, $mode) $needle = str_replace(self::CASE_FOLD[0], self::CASE_FOLD[1], $needle); } } - if ($reverse) { - $needlePos = strrpos($s, $needle); - } else { - $needlePos = strpos($s, $needle); + $needleLen = \strlen($needle); + $needlePos = $reverse ? strrpos($s, $needle) : strpos($s, $needle); + + while (false !== $needlePos) { + if (false !== $i = self::graphemeIndex($s, $needlePos, $needlePos + $needleLen)) { + return $i + $offset; + } + + if (!$reverse) { + $needlePos = strpos($s, $needle, $needlePos + 1); + } elseif ($needlePos) { + $needlePos = strrpos(substr($s, 0, $needlePos + $needleLen - 1), $needle); + } else { + $needlePos = false; + } + } + + return false; + } + + /** + * Returns the position in grapheme units of the bytes from $start to $end, + * or false when they split a grapheme cluster on characters that are not ignorable. + */ + private static function graphemeIndex($s, $start, $end) + { + if (self::isAsciiBoundary($s, $start) && self::isAsciiBoundary($s, $end)) { + return self::grapheme_strlen(substr($s, 0, $start)); } - return false !== $needlePos ? self::grapheme_strlen(substr($s, 0, $needlePos)) + $offset : false; + $i = $pos = 0; + + if ($start) { + // count the clusters that start before $start, then find where the last one starts + $prefix = substr($s, 0, $start); + preg_replace('/'.SYMFONY_GRAPHEME_CLUSTER_RX.'/u', '', $prefix, -1, $i); + + if (!$i) { + return false; + } + + $pos = 1 < $i ? $start - \strlen(preg_replace('/'.SYMFONY_GRAPHEME_CLUSTER_RX.'/u', '', $prefix, $i - 1)) : 0; + } + + do { + if (!preg_match('/\G'.SYMFONY_GRAPHEME_CLUSTER_RX.'/u', $s, $cluster, 0, $pos)) { + return false; + } + + $next = $pos + \strlen($cluster[0]); + + if ($pos < $start && $start < $next && !preg_match(self::IGNORABLE_RX, substr($s, $start, min($next, $end) - $start))) { + return false; + } + + if ($pos < $end && $end < $next && !preg_match(self::IGNORABLE_RX, substr($s, $end, $next - $end))) { + return false; + } + } while (($pos = $next) < $end); + + return $i; + } + + private static function isAsciiBoundary($s, $i) + { + return 0 === $i || !isset($s[$i]) || ($s[$i - 1] < "\x80" && $s[$i] < "\x80" && ("\r" !== $s[$i - 1] || "\n" !== $s[$i])); } public static function grapheme_strrev(string $string) diff --git a/upload/api/vendor/symfony/polyfill-mbstring/Mbstring.php b/upload/api/vendor/symfony/polyfill-mbstring/Mbstring.php index bc9e6acf..5a59d269 100644 --- a/upload/api/vendor/symfony/polyfill-mbstring/Mbstring.php +++ b/upload/api/vendor/symfony/polyfill-mbstring/Mbstring.php @@ -827,7 +827,10 @@ public static function mb_http_output($encoding = null) public static function mb_strwidth($s, $encoding = null) { $encoding = self::getEncoding($encoding); - $wideChars = '/[\x{1100}-\x{115F}\x{2329}\x{232A}\x{2E80}-\x{303E}\x{3040}-\x{A4CF}\x{AC00}-\x{D7A3}\x{F900}-\x{FAFF}\x{FE10}-\x{FE19}\x{FE30}-\x{FE6F}\x{FF00}-\x{FF60}\x{FFE0}-\x{FFE6}\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}]/u'; + static $wideChars = null; + if (null === $wideChars) { + $wideChars = self::getData('eastAsianWidthRegexp'); + } if ('UTF-8' !== $encoding) { $s = self::iconv($encoding, 'UTF-8', $s); @@ -1374,7 +1377,12 @@ private static function html_encoding_callback(array $m) private static function title_case(array $s) { - return self::mb_convert_case($s[1], \MB_CASE_UPPER, 'UTF-8').self::mb_convert_case($s[2], \MB_CASE_LOWER, 'UTF-8'); + static $titleCase = null; + if (null === $titleCase) { + $titleCase = self::getData('titleCase'); + } + + return ($titleCase[$s[1]] ?? self::mb_convert_case($s[1], \MB_CASE_UPPER, 'UTF-8')).self::mb_convert_case($s[2], \MB_CASE_LOWER, 'UTF-8'); } private static function getData($file) @@ -1414,6 +1422,10 @@ private static function getEncoding($encoding) return 'UTF-16BE'; } + if ('UCS-2' === $encoding) { + return 'UCS-2BE'; + } + return $encoding; } diff --git a/upload/api/vendor/symfony/polyfill-mbstring/Resources/unidata/eastAsianWidthRegexp.php b/upload/api/vendor/symfony/polyfill-mbstring/Resources/unidata/eastAsianWidthRegexp.php new file mode 100644 index 00000000..34d875e5 --- /dev/null +++ b/upload/api/vendor/symfony/polyfill-mbstring/Resources/unidata/eastAsianWidthRegexp.php @@ -0,0 +1,5 @@ + 'Dž', + 'Dž' => 'Dž', + 'dž' => 'Dž', + 'LJ' => 'Lj', + 'Lj' => 'Lj', + 'lj' => 'Lj', + 'NJ' => 'Nj', + 'Nj' => 'Nj', + 'nj' => 'Nj', + 'DZ' => 'Dz', + 'Dz' => 'Dz', + 'dz' => 'Dz', + 'ა' => 'ა', + 'ბ' => 'ბ', + 'გ' => 'გ', + 'დ' => 'დ', + 'ე' => 'ე', + 'ვ' => 'ვ', + 'ზ' => 'ზ', + 'თ' => 'თ', + 'ი' => 'ი', + 'კ' => 'კ', + 'ლ' => 'ლ', + 'მ' => 'მ', + 'ნ' => 'ნ', + 'ო' => 'ო', + 'პ' => 'პ', + 'ჟ' => 'ჟ', + 'რ' => 'რ', + 'ს' => 'ს', + 'ტ' => 'ტ', + 'უ' => 'უ', + 'ფ' => 'ფ', + 'ქ' => 'ქ', + 'ღ' => 'ღ', + 'ყ' => 'ყ', + 'შ' => 'შ', + 'ჩ' => 'ჩ', + 'ც' => 'ც', + 'ძ' => 'ძ', + 'წ' => 'წ', + 'ჭ' => 'ჭ', + 'ხ' => 'ხ', + 'ჯ' => 'ჯ', + 'ჰ' => 'ჰ', + 'ჱ' => 'ჱ', + 'ჲ' => 'ჲ', + 'ჳ' => 'ჳ', + 'ჴ' => 'ჴ', + 'ჵ' => 'ჵ', + 'ჶ' => 'ჶ', + 'ჷ' => 'ჷ', + 'ჸ' => 'ჸ', + 'ჹ' => 'ჹ', + 'ჺ' => 'ჺ', + 'ჽ' => 'ჽ', + 'ჾ' => 'ჾ', + 'ჿ' => 'ჿ', + 'ß' => 'Ss', + 'ff' => 'Ff', + 'fi' => 'Fi', + 'fl' => 'Fl', + 'ffi' => 'Ffi', + 'ffl' => 'Ffl', + 'ſt' => 'St', + 'st' => 'St', + 'և' => 'Եւ', + 'ﬓ' => 'Մն', + 'ﬔ' => 'Մե', + 'ﬕ' => 'Մի', + 'ﬖ' => 'Վն', + 'ﬗ' => 'Մխ', + 'ᾀ' => 'ᾈ', + 'ᾁ' => 'ᾉ', + 'ᾂ' => 'ᾊ', + 'ᾃ' => 'ᾋ', + 'ᾄ' => 'ᾌ', + 'ᾅ' => 'ᾍ', + 'ᾆ' => 'ᾎ', + 'ᾇ' => 'ᾏ', + 'ᾈ' => 'ᾈ', + 'ᾉ' => 'ᾉ', + 'ᾊ' => 'ᾊ', + 'ᾋ' => 'ᾋ', + 'ᾌ' => 'ᾌ', + 'ᾍ' => 'ᾍ', + 'ᾎ' => 'ᾎ', + 'ᾏ' => 'ᾏ', + 'ᾐ' => 'ᾘ', + 'ᾑ' => 'ᾙ', + 'ᾒ' => 'ᾚ', + 'ᾓ' => 'ᾛ', + 'ᾔ' => 'ᾜ', + 'ᾕ' => 'ᾝ', + 'ᾖ' => 'ᾞ', + 'ᾗ' => 'ᾟ', + 'ᾘ' => 'ᾘ', + 'ᾙ' => 'ᾙ', + 'ᾚ' => 'ᾚ', + 'ᾛ' => 'ᾛ', + 'ᾜ' => 'ᾜ', + 'ᾝ' => 'ᾝ', + 'ᾞ' => 'ᾞ', + 'ᾟ' => 'ᾟ', + 'ᾠ' => 'ᾨ', + 'ᾡ' => 'ᾩ', + 'ᾢ' => 'ᾪ', + 'ᾣ' => 'ᾫ', + 'ᾤ' => 'ᾬ', + 'ᾥ' => 'ᾭ', + 'ᾦ' => 'ᾮ', + 'ᾧ' => 'ᾯ', + 'ᾨ' => 'ᾨ', + 'ᾩ' => 'ᾩ', + 'ᾪ' => 'ᾪ', + 'ᾫ' => 'ᾫ', + 'ᾬ' => 'ᾬ', + 'ᾭ' => 'ᾭ', + 'ᾮ' => 'ᾮ', + 'ᾯ' => 'ᾯ', + 'ᾳ' => 'ᾼ', + 'ᾼ' => 'ᾼ', + 'ῃ' => 'ῌ', + 'ῌ' => 'ῌ', + 'ῳ' => 'ῼ', + 'ῼ' => 'ῼ', + 'ᾲ' => 'Ὰͅ', + 'ᾴ' => 'Άͅ', + 'ῂ' => 'Ὴͅ', + 'ῄ' => 'Ήͅ', + 'ῲ' => 'Ὼͅ', + 'ῴ' => 'Ώͅ', + 'ᾷ' => 'ᾼ͂', + 'ῇ' => 'ῌ͂', + 'ῷ' => 'ῼ͂', +); diff --git a/upload/api/vendor/symfony/string/AbstractString.php b/upload/api/vendor/symfony/string/AbstractString.php index 402839c5..65ddd2a0 100644 --- a/upload/api/vendor/symfony/string/AbstractString.php +++ b/upload/api/vendor/symfony/string/AbstractString.php @@ -685,7 +685,9 @@ public function wordwrap(int $width = 75, string $break = "\n", bool $cut = fals $string = ''; $j = 0; $b = $i = -1; - $mask = wordwrap($mask, $width, '#', $cut); + // wordwrap() does not recognize a break at the very end of its input and counts it as part of the last line + $wrapped = rtrim($mask, '#'); + $mask = wordwrap($wrapped, $width, '#', $cut).substr($mask, \strlen($wrapped)); while (false !== $b = strpos($mask, '#', $b + 1)) { for (++$i; $i < $b; ++$i) { diff --git a/upload/api/vendor/symfony/string/AbstractUnicodeString.php b/upload/api/vendor/symfony/string/AbstractUnicodeString.php index 6846d8e6..c6f71356 100644 --- a/upload/api/vendor/symfony/string/AbstractUnicodeString.php +++ b/upload/api/vendor/symfony/string/AbstractUnicodeString.php @@ -44,6 +44,9 @@ abstract class AbstractUnicodeString extends AbstractString private const TRANSLIT_FROM = ['Æ', 'Ð', 'Ø', 'Þ', 'ß', 'æ', 'ð', 'ø', 'þ', 'Đ', 'đ', 'Ħ', 'ħ', 'ı', 'ĸ', 'Ŀ', 'ŀ', 'Ł', 'ł', 'ʼn', 'Ŋ', 'ŋ', 'Œ', 'œ', 'Ŧ', 'ŧ', 'ƀ', 'Ɓ', 'Ƃ', 'ƃ', 'Ƈ', 'ƈ', 'Ɖ', 'Ɗ', 'Ƌ', 'ƌ', 'Ɛ', 'Ƒ', 'ƒ', 'Ɠ', 'ƕ', 'Ɩ', 'Ɨ', 'Ƙ', 'ƙ', 'ƚ', 'Ɲ', 'ƞ', 'Ƣ', 'ƣ', 'Ƥ', 'ƥ', 'ƫ', 'Ƭ', 'ƭ', 'Ʈ', 'Ʋ', 'Ƴ', 'ƴ', 'Ƶ', 'ƶ', 'DŽ', 'Dž', 'dž', 'Ǥ', 'ǥ', 'ȡ', 'Ȥ', 'ȥ', 'ȴ', 'ȵ', 'ȶ', 'ȷ', 'ȸ', 'ȹ', 'Ⱥ', 'Ȼ', 'ȼ', 'Ƚ', 'Ⱦ', 'ȿ', 'ɀ', 'Ƀ', 'Ʉ', 'Ɇ', 'ɇ', 'Ɉ', 'ɉ', 'Ɍ', 'ɍ', 'Ɏ', 'ɏ', 'ɓ', 'ɕ', 'ɖ', 'ɗ', 'ɛ', 'ɟ', 'ɠ', 'ɡ', 'ɢ', 'ɦ', 'ɧ', 'ɨ', 'ɪ', 'ɫ', 'ɬ', 'ɭ', 'ɱ', 'ɲ', 'ɳ', 'ɴ', 'ɶ', 'ɼ', 'ɽ', 'ɾ', 'ʀ', 'ʂ', 'ʈ', 'ʉ', 'ʋ', 'ʏ', 'ʐ', 'ʑ', 'ʙ', 'ʛ', 'ʜ', 'ʝ', 'ʟ', 'ʠ', 'ʣ', 'ʥ', 'ʦ', 'ʪ', 'ʫ', 'ᴀ', 'ᴁ', 'ᴃ', 'ᴄ', 'ᴅ', 'ᴆ', 'ᴇ', 'ᴊ', 'ᴋ', 'ᴌ', 'ᴍ', 'ᴏ', 'ᴘ', 'ᴛ', 'ᴜ', 'ᴠ', 'ᴡ', 'ᴢ', 'ᵫ', 'ᵬ', 'ᵭ', 'ᵮ', 'ᵯ', 'ᵰ', 'ᵱ', 'ᵲ', 'ᵳ', 'ᵴ', 'ᵵ', 'ᵶ', 'ᵺ', 'ᵻ', 'ᵽ', 'ᵾ', 'ᶀ', 'ᶁ', 'ᶂ', 'ᶃ', 'ᶄ', 'ᶅ', 'ᶆ', 'ᶇ', 'ᶈ', 'ᶉ', 'ᶊ', 'ᶌ', 'ᶍ', 'ᶎ', 'ᶏ', 'ᶑ', 'ᶒ', 'ᶓ', 'ᶖ', 'ᶙ', 'ẚ', 'ẜ', 'ẝ', 'ẞ', 'Ỻ', 'ỻ', 'Ỽ', 'ỽ', 'Ỿ', 'ỿ', '©', '®', '₠', '₢', '₣', '₤', '₧', '₺', '₹', 'ℌ', '℞', '㎧', '㎮', '㏆', '㏗', '㏞', '㏟', '¼', '½', '¾', '⅓', '⅔', '⅕', '⅖', '⅗', '⅘', '⅙', '⅚', '⅛', '⅜', '⅝', '⅞', '⅟', '〇', '‘', '’', '‚', '‛', '“', '”', '„', '‟', '′', '″', '〝', '〞', '«', '»', '‹', '›', '‐', '‑', '‒', '–', '—', '―', '︱', '︲', '﹘', '‖', '⁄', '⁅', '⁆', '⁎', '、', '。', '〈', '〉', '《', '》', '〔', '〕', '〘', '〙', '〚', '〛', '︑', '︒', '︹', '︺', '︽', '︾', '︿', '﹀', '﹑', '﹝', '﹞', '⦅', '⦆', '。', '、', '×', '÷', '−', '∕', '∖', '∣', '∥', '≪', '≫', '⦅', '⦆']; private const TRANSLIT_TO = ['AE', 'D', 'O', 'TH', 'ss', 'ae', 'd', 'o', 'th', 'D', 'd', 'H', 'h', 'i', 'q', 'L', 'l', 'L', 'l', '\'n', 'N', 'n', 'OE', 'oe', 'T', 't', 'b', 'B', 'B', 'b', 'C', 'c', 'D', 'D', 'D', 'd', 'E', 'F', 'f', 'G', 'hv', 'I', 'I', 'K', 'k', 'l', 'N', 'n', 'OI', 'oi', 'P', 'p', 't', 'T', 't', 'T', 'V', 'Y', 'y', 'Z', 'z', 'DZ', 'Dz', 'dz', 'G', 'g', 'd', 'Z', 'z', 'l', 'n', 't', 'j', 'db', 'qp', 'A', 'C', 'c', 'L', 'T', 's', 'z', 'B', 'U', 'E', 'e', 'J', 'j', 'R', 'r', 'Y', 'y', 'b', 'c', 'd', 'd', 'e', 'j', 'g', 'g', 'G', 'h', 'h', 'i', 'I', 'l', 'l', 'l', 'm', 'n', 'n', 'N', 'OE', 'r', 'r', 'r', 'R', 's', 't', 'u', 'v', 'Y', 'z', 'z', 'B', 'G', 'H', 'j', 'L', 'q', 'dz', 'dz', 'ts', 'ls', 'lz', 'A', 'AE', 'B', 'C', 'D', 'D', 'E', 'J', 'K', 'L', 'M', 'O', 'P', 'T', 'U', 'V', 'W', 'Z', 'ue', 'b', 'd', 'f', 'm', 'n', 'p', 'r', 'r', 's', 't', 'z', 'th', 'I', 'p', 'U', 'b', 'd', 'f', 'g', 'k', 'l', 'm', 'n', 'p', 'r', 's', 'v', 'x', 'z', 'a', 'd', 'e', 'e', 'i', 'u', 'a', 's', 's', 'SS', 'LL', 'll', 'V', 'v', 'Y', 'y', '(C)', '(R)', 'CE', 'Cr', 'Fr.', 'L.', 'Pts', 'TL', 'Rs', 'x', 'Rx', 'm/s', 'rad/s', 'C/kg', 'pH', 'V/m', 'A/m', ' 1/4', ' 1/2', ' 3/4', ' 1/3', ' 2/3', ' 1/5', ' 2/5', ' 3/5', ' 4/5', ' 1/6', ' 5/6', ' 1/8', ' 3/8', ' 5/8', ' 7/8', ' 1/', '0', '\'', '\'', ',', '\'', '"', '"', ',,', '"', '\'', '"', '"', '"', '<<', '>>', '<', '>', '-', '-', '-', '-', '-', '-', '-', '-', '-', '||', '/', '[', ']', '*', ',', '.', '<', '>', '<<', '>>', '[', ']', '[', ']', '[', ']', ',', '.', '[', ']', '<<', '>>', '<', '>', ',', '[', ']', '((', '))', '.', ',', '*', '/', '-', '/', '\\', '|', '||', '<<', '>>', '((', '))']; + // \p{ExtPict} requires PCRE2 10.40; U+1F1E6-1F1FF are regional indicators, #*0-9 keycap bases + private const EMOJI_REGEX = \PCRE_VERSION_MAJOR < 10 || \PCRE_VERSION_MAJOR === 10 && \PCRE_VERSION_MINOR < 40 ? '/[\p{So}#*0-9]/u' : '/[\p{ExtPict}\x{1F1E6}-\x{1F1FF}#*0-9]/u'; + private static array $transliterators = []; private static array $tableZero; private static array $tableWide; @@ -402,10 +405,16 @@ public function localeTitle(string $locale): static public function trim(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static { - if (" \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}" !== $chars && !preg_match('//u', $chars)) { + if ('' === $chars) { + return clone $this; + } + if (" \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}" === $chars) { + $chars = preg_quote($chars); + } elseif (false === $ranges = preg_match('/\.\./u', $chars)) { throw new InvalidArgumentException('Invalid UTF-8 chars.'); + } else { + $chars = $ranges ? self::quoteCharRanges($chars) : preg_quote($chars); } - $chars = preg_quote($chars); $str = clone $this; $str->string = preg_replace("{^[$chars]++|[$chars]++$}uD", '', $str->string); @@ -415,10 +424,16 @@ public function trim(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static public function trimEnd(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static { - if (" \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}" !== $chars && !preg_match('//u', $chars)) { + if ('' === $chars) { + return clone $this; + } + if (" \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}" === $chars) { + $chars = preg_quote($chars); + } elseif (false === $ranges = preg_match('/\.\./u', $chars)) { throw new InvalidArgumentException('Invalid UTF-8 chars.'); + } else { + $chars = $ranges ? self::quoteCharRanges($chars) : preg_quote($chars); } - $chars = preg_quote($chars); $str = clone $this; $str->string = preg_replace("{[$chars]++$}uD", '', $str->string); @@ -448,10 +463,16 @@ public function trimPrefix($prefix): static public function trimStart(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static { - if (" \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}" !== $chars && !preg_match('//u', $chars)) { + if ('' === $chars) { + return clone $this; + } + if (" \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}" === $chars) { + $chars = preg_quote($chars); + } elseif (false === $ranges = preg_match('/\.\./u', $chars)) { throw new InvalidArgumentException('Invalid UTF-8 chars.'); + } else { + $chars = $ranges ? self::quoteCharRanges($chars) : preg_quote($chars); } - $chars = preg_quote($chars); $str = clone $this; $str->string = preg_replace("{^[$chars]++}uD", '', $str->string); @@ -505,7 +526,14 @@ public function localeUpper(string $locale): static public function width(bool $ignoreAnsiDecoration = true): int { $width = 0; - $s = str_replace(["\x00", "\x05", "\x07"], '', $this->string); + $s = $this->string; + + if ($ignoreAnsiDecoration && str_contains($s, "\x07")) { + // BEL can terminate an OSC sequence instead of ST: strip such sequences before BEL is removed + $s = preg_replace('/\x1B\][^\x07\x1B\n\r]*+\x07/', '', $s); + } + + $s = str_replace(["\x00", "\x05", "\x07"], '', $s); if (str_contains($s, "\r")) { $s = str_replace(["\r\n", "\r"], "\n", $s); @@ -520,7 +548,7 @@ public function width(bool $ignoreAnsiDecoration = true): int $s = preg_replace('/(?:\x1B(?: \[ [\x30-\x3F]*+ [\x20-\x2F]*+ [\x40-\x7E] | [P\]X^_] .*? \x1B\\\\ - | [\x41-\x7E] + | [\x20-\x2F]*+ [\x30-\x7E]? )|[\p{Cc}\x7F]++)/xu', '', $s); } @@ -534,6 +562,27 @@ public function width(bool $ignoreAnsiDecoration = true): int return $width; } + /** + * Quotes $chars for a character class, reading "x..y" as a range of code points like trim() does with bytes. + */ + private static function quoteCharRanges(string $chars): string + { + $chars = preg_split('//u', $chars, -1, \PREG_SPLIT_NO_EMPTY); + $n = \count($chars); + $class = ''; + + for ($i = 0; $i < $n; ++$i) { + if ($i + 3 < $n && '.' === $chars[$i + 1] && '.' === $chars[$i + 2] && ($from = mb_ord($chars[$i], 'UTF-8')) <= ($to = mb_ord($chars[$i + 3], 'UTF-8'))) { + $class .= \sprintf('\x{%X}-\x{%X}', $from, $to); + $i += 3; + } elseif ('.' !== $chars[$i] || '.' !== ($chars[$i + 1] ?? '')) { + $class .= preg_quote($chars[$i]); + } + } + + return $class; + } + private function pad(int $len, self $pad, int $type): static { $sLen = $this->length(); @@ -578,35 +627,53 @@ private function wcswidth(string $string): int $width = 0; $lastChar = null; $lastWidth = null; + $joined = false; foreach (preg_split('//u', $string, -1, \PREG_SPLIT_NO_EMPTY) as $c) { + if ($joined) { + $joined = false; + + if ("\x7F" < $c[0] && preg_match(self::EMOJI_REGEX, $c)) { + continue; + } + } + $codePoint = mb_ord($c, 'UTF-8'); + if (0x7F > $codePoint && 0x20 <= $codePoint) { // printable ASCII + ++$width; + $lastChar = $c; + $lastWidth = 1; + + continue; + } + if (0 === $codePoint // NULL || 0x034F === $codePoint // COMBINING GRAPHEME JOINER + || 0x061C === $codePoint // ARABIC LETTER MARK + || (0x1160 <= $codePoint && 0x11FF >= $codePoint) // HANGUL JUNGSEONG FILLER to HANGUL JONGSEONG SSANGNIEUN + || 0x180E === $codePoint // MONGOLIAN VOWEL SEPARATOR || (0x200B <= $codePoint && 0x200F >= $codePoint) // ZERO WIDTH SPACE to RIGHT-TO-LEFT MARK || 0x2028 === $codePoint // LINE SEPARATOR || 0x2029 === $codePoint // PARAGRAPH SEPARATOR || (0x202A <= $codePoint && 0x202E >= $codePoint) // LEFT-TO-RIGHT EMBEDDING to RIGHT-TO-LEFT OVERRIDE - || (0x2060 <= $codePoint && 0x2063 >= $codePoint) // WORD JOINER to INVISIBLE SEPARATOR + || (0x2060 <= $codePoint && 0x206F >= $codePoint) // WORD JOINER to NOMINAL DIGIT SHAPES + || (0xD7B0 <= $codePoint && 0xD7FF >= $codePoint) // HANGUL JAMO EXTENDED-B + || 0xFEFF === $codePoint // ZERO WIDTH NO-BREAK SPACE + || (0xFFF0 <= $codePoint && 0xFFFB >= $codePoint) // INTERLINEAR ANNOTATION ANCHOR to TERMINATOR, and the unassigned code points before them ) { + // after an emoji, a ZERO WIDTH JOINER makes the next character part of it + $joined = 0x200D === $codePoint && null !== $lastChar && 1 < \strlen($lastChar) && preg_match(self::EMOJI_REGEX, $lastChar); + continue; } - // Non printable characters - if (32 > $codePoint // C0 control characters - || (0x07F <= $codePoint && 0x0A0 > $codePoint) // C1 control characters and DEL - ) { + if (0x0A0 > $codePoint) { // C0 and C1 control characters and DEL return -1; } if (0xFE0F === $codePoint) { - if (\PCRE_VERSION_MAJOR < 10 || \PCRE_VERSION_MAJOR === 10 && \PCRE_VERSION_MINOR < 40) { - $regex = '/\p{So}/u'; - } else { - $regex = '/\p{Emoji}/u'; - } - if (null !== $lastChar && 1 === $lastWidth && preg_match($regex, $lastChar)) { + if (null !== $lastChar && 1 === $lastWidth && preg_match(self::EMOJI_REGEX, $lastChar)) { ++$width; $lastWidth = 2; } @@ -614,6 +681,26 @@ private function wcswidth(string $string): int continue; } + if (0xFFFF < $codePoint) { + if ((0x13430 <= $codePoint && 0x1343F >= $codePoint) // EGYPTIAN HIEROGLYPH VERTICAL JOINER to EGYPTIAN HIEROGLYPH END WALLED ENCLOSURE + || (0x1BCA0 <= $codePoint && 0x1BCA3 >= $codePoint) // SHORTHAND FORMAT LETTER OVERLAP to SHORTHAND FORMAT UP STEP + || (0x1D173 <= $codePoint && 0x1D17A >= $codePoint) // MUSICAL SYMBOL BEGIN BEAM to MUSICAL SYMBOL END PHRASE + || (0xE0000 <= $codePoint && 0xE0FFF >= $codePoint) // TAGS and the default ignorable code points around them + ) { + continue; + } + + if (0x1F3FB <= $codePoint && 0x1F3FF >= $codePoint && null !== $lastChar && 1 < \strlen($lastChar) && preg_match(self::EMOJI_REGEX, $lastChar)) { // EMOJI MODIFIER FITZPATRICK TYPE-1-2 to TYPE-6, not after a keycap base + // like VS16, a skin tone gives a text-default emoji its emoji presentation + if (1 === $lastWidth) { + ++$width; + $lastWidth = 2; + } + + continue; + } + } + self::$tableZero ??= require __DIR__.'/Resources/data/wcswidth_table_zero.php'; if ($codePoint >= self::$tableZero[0][0] && $codePoint <= self::$tableZero[$ubound = \count(self::$tableZero) - 1][1]) { diff --git a/upload/api/vendor/symfony/string/CodePointString.php b/upload/api/vendor/symfony/string/CodePointString.php index f699168e..97e4520f 100644 --- a/upload/api/vendor/symfony/string/CodePointString.php +++ b/upload/api/vendor/symfony/string/CodePointString.php @@ -55,6 +55,10 @@ public function chunk(int $length = 1): array return []; } + if (\strlen($this->string) <= $length) { + return [clone $this]; + } + $rx = '/('; while (65535 < $length) { $rx .= '.{65535}'; @@ -197,7 +201,7 @@ public function replace(string $from, string $to): static public function slice(int $start = 0, ?int $length = null): static { $str = clone $this; - $str->string = mb_substr($this->string, $start, $length, 'UTF-8'); + $str->string = mb_substr($this->string, max(-\PHP_INT_MAX, $start), max(-\PHP_INT_MAX, $length ?? \PHP_INT_MAX), 'UTF-8'); return $str; } @@ -209,8 +213,8 @@ public function splice(string $replacement, int $start = 0, ?int $length = null) } $str = clone $this; - $start = $start ? \strlen(mb_substr($this->string, 0, $start, 'UTF-8')) : 0; - $length = $length ? \strlen(mb_substr(substr($this->string, $start), 0, $length, 'UTF-8')) : $length; + $start = $start ? \strlen(mb_substr($this->string, 0, max(-\PHP_INT_MAX, $start), 'UTF-8')) : 0; + $length = $length ? \strlen(mb_substr(substr($this->string, $start), 0, max(-\PHP_INT_MAX, $length), 'UTF-8')) : $length; $str->string = substr_replace($this->string, $replacement, $start, $length ?? \PHP_INT_MAX); return $str; diff --git a/upload/api/vendor/symfony/string/Resources/data/wcswidth_table_wide.php b/upload/api/vendor/symfony/string/Resources/data/wcswidth_table_wide.php index 6a750942..6b180ce2 100644 --- a/upload/api/vendor/symfony/string/Resources/data/wcswidth_table_wide.php +++ b/upload/api/vendor/symfony/string/Resources/data/wcswidth_table_wide.php @@ -3,8 +3,8 @@ /* * This file has been auto-generated by the Symfony String Component for internal use. * - * Unicode version: 16.0.0 - * Date: 2024-09-11T08:21:22+00:00 + * Unicode version: 18.0.0 + * Date: 2026-09-29T20:21:16+00:00 */ return [ @@ -836,9 +836,17 @@ 94192, 94193, ], + [ + 94194, + 94195, + ], + [ + 94196, + 94198, + ], [ 94208, - 100343, + 100351, ], [ 100352, @@ -846,7 +854,7 @@ ], [ 101120, - 101589, + 101594, ], [ 101631, @@ -854,7 +862,19 @@ ], [ 101632, - 101640, + 101664, + ], + [ + 101760, + 101874, + ], + [ + 101888, + 102801, + ], + [ + 102816, + 102866, ], [ 110576, @@ -874,7 +894,7 @@ ], [ 110848, - 110882, + 110888, ], [ 110898, @@ -890,7 +910,7 @@ ], [ 110948, - 110951, + 110952, ], [ 110960, @@ -920,6 +940,10 @@ 127377, 127386, ], + [ + 127406, + 127406, + ], [ 127488, 127490, @@ -1038,7 +1062,7 @@ ], [ 128725, - 128727, + 128729, ], [ 128732, @@ -1052,6 +1076,10 @@ 128756, 128764, ], + [ + 128986, + 128986, + ], [ 128992, 129003, @@ -1078,23 +1106,23 @@ ], [ 129664, - 129673, + 129734, ], [ - 129679, - 129734, + 129736, + 129736, ], [ - 129742, - 129756, + 129740, + 129757, ], [ 129759, - 129769, + 129771, ], [ - 129776, - 129784, + 129775, + 129786, ], [ 131072, @@ -1106,26 +1134,18 @@ ], [ 173824, - 177977, - ], - [ - 177978, - 177983, - ], - [ - 177984, - 178205, + 178206, ], [ - 178206, + 178207, 178207, ], [ 178208, - 183969, + 183981, ], [ - 183970, + 183982, 183983, ], [ @@ -1166,10 +1186,18 @@ ], [ 201552, - 205743, + 210041, + ], + [ + 210042, + 249855, + ], + [ + 249856, + 261183, ], [ - 205744, + 261184, 262141, ], ]; diff --git a/upload/api/vendor/symfony/string/Resources/data/wcswidth_table_zero.php b/upload/api/vendor/symfony/string/Resources/data/wcswidth_table_zero.php index fdd7f3c7..1372a1e4 100644 --- a/upload/api/vendor/symfony/string/Resources/data/wcswidth_table_zero.php +++ b/upload/api/vendor/symfony/string/Resources/data/wcswidth_table_zero.php @@ -3,8 +3,8 @@ /* * This file has been auto-generated by the Symfony String Component for internal use. * - * Unicode version: 16.0.0 - * Date: 2024-09-11T08:21:22+00:00 + * Unicode version: 18.0.0 + * Date: 2026-09-29T20:21:16+00:00 */ return [ @@ -38,7 +38,7 @@ ], [ 1479, - 1479, + 1481, ], [ 1552, @@ -249,7 +249,7 @@ 2893, ], [ - 2901, + 2899, 2902, ], [ @@ -586,7 +586,7 @@ ], [ 6847, - 6862, + 6896, ], [ 6912, @@ -925,7 +925,11 @@ 69292, ], [ - 69372, + 69323, + 69327, + ], + [ + 69360, 69375, ], [ @@ -1224,6 +1228,18 @@ 72344, 72345, ], + [ + 72544, + 72544, + ], + [ + 72546, + 72548, + ], + [ + 72550, + 72550, + ], [ 72752, 72758, @@ -1284,6 +1300,10 @@ 73111, 73111, ], + [ + 73200, + 73200, + ], [ 73459, 73460, @@ -1356,6 +1376,10 @@ 118576, 118598, ], + [ + 119079, + 119080, + ], [ 119143, 119145, @@ -1376,6 +1400,10 @@ 119362, 119364, ], + [ + 119387, + 119388, + ], [ 121344, 121398, @@ -1444,6 +1472,22 @@ 124398, 124399, ], + [ + 124643, + 124643, + ], + [ + 124646, + 124646, + ], + [ + 124654, + 124655, + ], + [ + 124661, + 124661, + ], [ 125136, 125142, diff --git a/upload/api/vendor/symfony/string/Slugger/AsciiSlugger.php b/upload/api/vendor/symfony/string/Slugger/AsciiSlugger.php index 689a4047..e15497c5 100644 --- a/upload/api/vendor/symfony/string/Slugger/AsciiSlugger.php +++ b/upload/api/vendor/symfony/string/Slugger/AsciiSlugger.php @@ -63,7 +63,7 @@ class AsciiSlugger implements SluggerInterface, LocaleAwareInterface /** * Cache of transliterators per locale. * - * @var \Transliterator[] + * @var (\Transliterator|null)[] */ private array $transliterators = []; @@ -106,7 +106,7 @@ public function slug(string $string, string $separator = '-', ?string $locale = $locale ??= $this->defaultLocale; $transliterator = []; - if ($locale && ('de' === $locale || str_starts_with($locale, 'de_'))) { + if ($locale && ('de' === $locale || str_starts_with($locale, 'de_') || str_starts_with($locale, 'de-'))) { // Use the shortcut for German in UnicodeString::ascii() if possible (faster and no requirement on intl) $transliterator = ['de-ASCII']; } elseif (\function_exists('transliterator_transliterate') && $locale) { @@ -151,6 +151,8 @@ public function slug(string $string, string $separator = '-', ?string $locale = private function createTransliterator(string $locale): ?\Transliterator { + $locale = str_replace('-', '_', $locale); + if (\array_key_exists($locale, $this->transliterators)) { return $this->transliterators[$locale]; } @@ -167,10 +169,10 @@ private function createTransliterator(string $locale): ?\Transliterator // Try to use the parent locale (ie. try "de" for "de_AT") and cache both locales if ($id = self::LOCALE_TO_TRANSLITERATOR_ID[$parent] ?? null) { - $transliterator = \Transliterator::create($id.'/BGN') ?? \Transliterator::create($id); + return $this->transliterators[$locale] = $this->transliterators[$parent] = \Transliterator::create($id.'/BGN') ?? \Transliterator::create($id); } - return $this->transliterators[$locale] = $this->transliterators[$parent] = $transliterator ?? null; + return $this->transliterators[$locale] = null; } private function createEmojiTransliterator(?string $locale): ?EmojiTransliterator @@ -181,6 +183,8 @@ private function createEmojiTransliterator(?string $locale): ?EmojiTransliterato return null; } + $locale = null !== $locale ? str_replace('-', '_', $locale) : null; + while (null !== $locale) { try { return EmojiTransliterator::create("emoji-$locale"); @@ -197,11 +201,11 @@ private static function getParentLocale(?string $locale): ?string if (!$locale) { return null; } - if (false === $str = strrchr($locale, '_')) { + if (false === $i = strrpos(str_replace('-', '_', $locale), '_')) { // no parent locale return null; } - return substr($locale, 0, -\strlen($str)); + return substr($locale, 0, $i); } } diff --git a/upload/api/vendor/symfony/string/UnicodeString.php b/upload/api/vendor/symfony/string/UnicodeString.php index ca3499ff..d23833b0 100644 --- a/upload/api/vendor/symfony/string/UnicodeString.php +++ b/upload/api/vendor/symfony/string/UnicodeString.php @@ -73,6 +73,10 @@ public function chunk(int $length = 1): array return []; } + if (\strlen($this->string) <= $length) { + return [clone $this]; + } + $rx = '/('; while (65535 < $length) { $rx .= '\X{65535}'; @@ -179,7 +183,7 @@ public function indexOfLast(string|iterable|AbstractString $needle, int $offset if (0 > $offset) { // workaround https://bugs.php.net/74264 if (0 > $offset += grapheme_strlen($needle)) { - $string = grapheme_substr($string, 0, $offset); + $string = grapheme_substr($string, 0, max(-2147483648, $offset)); } $offset = 0; } @@ -248,12 +252,10 @@ public function replace(string $from, string $to): static if ('' !== $from && false !== $from) { $tail = $str->string; $result = ''; - $indexOf = $this->ignoreCase ? 'grapheme_stripos' : 'grapheme_strpos'; - while ('' !== $tail && false !== $i = $indexOf($tail, $from)) { - $slice = grapheme_substr($tail, 0, $i); - $result .= $slice.$to; - $tail = substr($tail, \strlen($slice) + \strlen($from)); + while ('' !== $tail && false !== $i = $this->search($tail, $from, $length)) { + $result .= substr($tail, 0, $i).$to; + $tail = substr($tail, $i + $length); } $str->string = $result.$tail; @@ -284,7 +286,10 @@ public function slice(int $start = 0, ?int $length = null): static { $str = clone $this; - $str->string = (string) grapheme_substr($this->string, $start, $length ?? 2147483647); + // grapheme_substr() accepts only 32-bit offsets and lengths + $start = max(-2147483648, min($start, 2147483647)); + $length = max(-2147483648, min($length ?? 2147483647, 2147483647)); + $str->string = (string) grapheme_substr($this->string, $start, $length); return $str; } @@ -293,8 +298,8 @@ public function splice(string $replacement, int $start = 0, ?int $length = null) { $str = clone $this; - $start = $start ? \strlen(grapheme_substr($this->string, 0, $start)) : 0; - $length = $length ? \strlen(grapheme_substr(substr($this->string, $start), 0, $length)) : $length; + $start = $start ? \strlen(grapheme_substr($this->string, 0, max(-2147483648, min($start, 2147483647)))) : 0; + $length = $length ? \strlen(grapheme_substr(substr($this->string, $start), 0, max(-2147483648, min($length, 2147483647)))) : $length; $str->string = substr_replace($this->string, $replacement, $start, $length ?? 2147483647); if (normalizer_is_normalized($str->string)) { @@ -333,12 +338,11 @@ public function split(string $delimiter, ?int $limit = null, ?int $flags = null) $str = clone $this; $tail = $this->string; $chunks = []; - $indexOf = $this->ignoreCase ? 'grapheme_stripos' : 'grapheme_strpos'; - while (1 < $limit && false !== $i = $indexOf($tail, $delimiter)) { - $str->string = grapheme_substr($tail, 0, $i); + while (1 < $limit && false !== $i = $this->search($tail, $delimiter, $length)) { + $str->string = substr($tail, 0, $i); $chunks[] = clone $str; - $tail = substr($tail, \strlen($str->string) + \strlen($delimiter)); + $tail = substr($tail, $i + $length); --$limit; } @@ -473,4 +477,49 @@ public function __clone() $this->ignoreCase = false; } + + /** + * Returns the byte offset of the first occurrence of $needle in $haystack and sets $length to its length in bytes. + */ + private function search(string $haystack, string $needle, ?int &$length = null): int|false + { + if (false === $i = $this->ignoreCase ? grapheme_stripos($haystack, $needle) : grapheme_strpos($haystack, $needle)) { + return false; + } + + $offset = \strlen(grapheme_substr($haystack, 0, $i)); + $length = \strlen($needle); + $match = substr($haystack, $offset, $length); + + // On non-ASCII strings, grapheme_strpos() ignores characters like U+200B. + // Its match can then differ from $needle, or start after the first characters of $needle. + // Without ext-intl, a match that starts inside a grapheme is reported at the next grapheme. + if (!$this->ignoreCase) { + if ($needle === $match) { + return $offset; + } + + $previous = $i ? \strlen(grapheme_substr($haystack, 0, $i - 1)) : 0; + + return strpos($haystack, $needle, max(0, min($offset + 1 - $length, $previous + 1))); + } + + if (0 === strcasecmp($needle, $match) || (preg_match('//u', $match) && ( + mb_convert_case($needle, \MB_CASE_FOLD_SIMPLE, 'UTF-8') === mb_convert_case($match, \MB_CASE_FOLD_SIMPLE, 'UTF-8') + || (0 === grapheme_stripos($needle, $match) && 0 === grapheme_stripos($match, $needle)) + ))) { + return $offset; + } + + $previous = $i ? mb_strlen(grapheme_substr($haystack, 0, $i - 1), 'UTF-8') : 0; + $i = mb_strlen(substr($haystack, 0, $offset), 'UTF-8'); + + if (false === $i = mb_stripos($haystack, $needle, max(0, min($i + 1 - mb_strlen($needle, 'UTF-8'), $previous + 1)), 'UTF-8')) { + return false; + } + + $length = \strlen(mb_substr($haystack, $i, mb_strlen($needle, 'UTF-8'), 'UTF-8')); + + return \strlen(mb_substr($haystack, 0, $i, 'UTF-8')); + } } diff --git a/upload/api/vendor/symfony/type-info/TypeResolver/StringTypeResolver.php b/upload/api/vendor/symfony/type-info/TypeResolver/StringTypeResolver.php index 105311d9..771c546c 100644 --- a/upload/api/vendor/symfony/type-info/TypeResolver/StringTypeResolver.php +++ b/upload/api/vendor/symfony/type-info/TypeResolver/StringTypeResolver.php @@ -95,10 +95,7 @@ public function resolve(mixed $subject, ?TypeContext $typeContext = null): Type } try { - $tokens = new TokenIterator($this->lexer->tokenize($subject)); - $node = $this->parser->parse($tokens); - - return $this->getTypeFromNode($node, $typeContext); + return $this->getTypeFromNode($this->parse($subject), $typeContext); } catch (\DomainException $e) { throw new UnsupportedException(\sprintf('Cannot resolve "%s".', $subject), $subject, previous: $e); } @@ -209,44 +206,7 @@ private function getTypeFromNode(TypeNode $node, ?TypeContext $typeContext): Typ } if ($node instanceof IdentifierTypeNode) { - $type = match ($node->name) { - 'bool', 'boolean' => Type::bool(), - 'true' => Type::true(), - 'false' => Type::false(), - 'int', 'integer', 'positive-int', 'negative-int', 'non-positive-int', 'non-negative-int', 'non-zero-int' => Type::int(), - 'float', 'double' => Type::float(), - 'string', - 'class-string', - 'trait-string', - 'interface-string', - 'callable-string', - 'numeric-string', - 'lowercase-string', - 'non-empty-lowercase-string', - 'non-empty-string', - 'non-falsy-string', - 'truthy-string', - 'literal-string', - 'html-escaped-string' => Type::string(), - 'resource' => Type::resource(), - 'object' => Type::object(), - 'callable' => Type::callable(), - 'array', 'non-empty-array' => Type::array(), - 'list', 'non-empty-list' => Type::list(), - 'iterable' => Type::iterable(), - 'mixed' => Type::mixed(), - 'null' => Type::null(), - 'array-key' => Type::arrayKey(), - 'scalar' => Type::union(Type::int(), Type::float(), Type::string(), Type::bool()), - 'number' => Type::union(Type::int(), Type::float()), - 'numeric' => Type::union(Type::int(), Type::float(), Type::string()), - 'self' => $typeContext ? Type::object($typeContext->getDeclaringClass()) : throw new InvalidArgumentException(\sprintf('A "%s" must be provided to resolve "self".', TypeContext::class)), - 'static' => $typeContext ? Type::object($typeContext->getCalledClass()) : throw new InvalidArgumentException(\sprintf('A "%s" must be provided to resolve "static".', TypeContext::class)), - 'parent' => $typeContext ? Type::object($typeContext->getParentClass()) : throw new InvalidArgumentException(\sprintf('A "%s" must be provided to resolve "parent".', TypeContext::class)), - 'void' => Type::void(), - 'never', 'never-return', 'never-returns', 'no-return' => Type::never(), - default => $this->resolveCustomIdentifier($node->name, $typeContext), - }; + $type = $this->resolveBuiltinIdentifier($node->name, $typeContext) ?? $this->resolveCustomIdentifier($node->name, $typeContext); if ($typeIsCollectionObject($type)) { return Type::collection($type); @@ -260,7 +220,17 @@ private function getTypeFromNode(TypeNode $node, ?TypeContext $typeContext): Typ } if ($node instanceof GenericTypeNode) { - if ($node->type instanceof IdentifierTypeNode && 'value-of' === $node->type->name) { + $type = null; + $keyword = $node->type instanceof IdentifierTypeNode ? $node->type->name : null; + if (null !== $keyword && ($lowercase = strtolower($keyword)) !== $keyword && \in_array($lowercase, ['value-of', 'key-of'], true)) { + try { + $type = $this->getTypeFromNode($node->type, $typeContext); + } catch (\DomainException) { + $keyword = $lowercase; + } + } + + if ('value-of' === $keyword) { $type = $this->getTypeFromNode($node->genericTypes[0], $typeContext); if ($type instanceof BackedEnumType) { return $type->getBackingType(); @@ -273,7 +243,7 @@ private function getTypeFromNode(TypeNode $node, ?TypeContext $typeContext): Typ throw new \DomainException(\sprintf('"%s" is not a valid type for "value-of".', $node->genericTypes[0])); } - if ($node->type instanceof IdentifierTypeNode && 'key-of' === $node->type->name) { + if ('key-of' === $keyword) { $type = $this->getTypeFromNode($node->genericTypes[0], $typeContext); if ($type instanceof CollectionType) { return $type->getCollectionKeyType(); @@ -282,7 +252,7 @@ private function getTypeFromNode(TypeNode $node, ?TypeContext $typeContext): Typ throw new \DomainException(\sprintf('"%s" is not a valid type for "key-of".', $node->genericTypes[0])); } - $type = $this->getTypeFromNode($node->type, $typeContext); + $type ??= $this->getTypeFromNode($node->type, $typeContext); // handle integer ranges as simple integers if ($type->isIdentifiedBy(TypeIdentifier::INT)) { @@ -349,6 +319,48 @@ private function getTypeFromNode(TypeNode $node, ?TypeContext $typeContext): Typ throw new \DomainException(\sprintf('Unhandled "%s" node.', $node::class)); } + private function resolveBuiltinIdentifier(string $identifier, ?TypeContext $typeContext): ?Type + { + return match ($identifier) { + 'bool', 'boolean' => Type::bool(), + 'true' => Type::true(), + 'false' => Type::false(), + 'int', 'integer', 'positive-int', 'negative-int', 'non-positive-int', 'non-negative-int', 'non-zero-int' => Type::int(), + 'float', 'double' => Type::float(), + 'string', + 'class-string', + 'trait-string', + 'interface-string', + 'callable-string', + 'numeric-string', + 'lowercase-string', + 'non-empty-lowercase-string', + 'non-empty-string', + 'non-falsy-string', + 'truthy-string', + 'literal-string', + 'html-escaped-string' => Type::string(), + 'resource' => Type::resource(), + 'object' => Type::object(), + 'callable' => Type::callable(), + 'array', 'non-empty-array' => Type::array(), + 'list', 'non-empty-list' => Type::list(), + 'iterable' => Type::iterable(), + 'mixed' => Type::mixed(), + 'null' => Type::null(), + 'array-key' => Type::arrayKey(), + 'scalar' => Type::union(Type::int(), Type::float(), Type::string(), Type::bool()), + 'number' => Type::union(Type::int(), Type::float()), + 'numeric' => Type::union(Type::int(), Type::float(), Type::string()), + 'self' => $typeContext ? Type::object($typeContext->getDeclaringClass()) : throw new InvalidArgumentException(\sprintf('A "%s" must be provided to resolve "self".', TypeContext::class)), + 'static' => $typeContext ? Type::object($typeContext->getCalledClass()) : throw new InvalidArgumentException(\sprintf('A "%s" must be provided to resolve "static".', TypeContext::class)), + 'parent' => $typeContext ? Type::object($typeContext->getParentClass()) : throw new InvalidArgumentException(\sprintf('A "%s" must be provided to resolve "parent".', TypeContext::class)), + 'void' => Type::void(), + 'never', 'never-return', 'never-returns', 'no-return' => Type::never(), + default => null, + }; + } + private function resolveCustomIdentifier(string $identifier, ?TypeContext $typeContext): Type { $className = $typeContext ? $typeContext->normalize($identifier) : $identifier; @@ -397,6 +409,33 @@ private function resolveCustomIdentifier(string $identifier, ?TypeContext $typeC } } + if (($keyword = strtolower($identifier)) !== $identifier && null !== $type = $this->resolveBuiltinIdentifier($keyword, $typeContext)) { + return $type; + } + throw new \DomainException(\sprintf('Unhandled "%s" identifier.', $identifier)); } + + private function parse(string $subject): TypeNode + { + $lexerTokens = $this->lexer->tokenize($subject); + $tokens = new TokenIterator($lexerTokens); + $node = $this->parser->parse($tokens); + + while ($tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL)) { + } + + $i = $tokens->currentTokenIndex(); + if (Lexer::TOKEN_OPEN_CURLY_BRACKET === ($lexerTokens[$i][Lexer::TYPE_OFFSET] ?? null)) { + while (\in_array($lexerTokens[--$i][Lexer::TYPE_OFFSET] ?? null, [Lexer::TOKEN_HORIZONTAL_WS, Lexer::TOKEN_PHPDOC_EOL], true)) { + } + + $value = $lexerTokens[$i][Lexer::VALUE_OFFSET] ?? ''; + if (($keyword = strtolower($value)) !== $value && \in_array($keyword, ['array', 'list', 'non-empty-array', 'non-empty-list'], true)) { + throw new \DomainException(\sprintf('Use "%s{...}" instead of "%s{...}" for shape types.', $keyword, $value)); + } + } + + return $node; + } } diff --git a/upload/api/vendor/symfony/yaml/Dumper.php b/upload/api/vendor/symfony/yaml/Dumper.php index cd5a1f6c..8b92b8a5 100644 --- a/upload/api/vendor/symfony/yaml/Dumper.php +++ b/upload/api/vendor/symfony/yaml/Dumper.php @@ -76,26 +76,9 @@ private function doDump(mixed $input, int $inline = 0, int $indent = 0, int $fla $key = (string) $key; } - if (Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK & $flags && \is_string($value) && str_contains($value, "\n") && !str_contains($value, "\r")) { - $blockIndentationIndicator = $this->getBlockIndentationIndicator($value); - - if (isset($value[-2]) && "\n" === $value[-2] && "\n" === $value[-1]) { - $blockChompingIndicator = '+'; - } elseif ("\n" === $value[-1]) { - $blockChompingIndicator = ''; - } else { - $blockChompingIndicator = '-'; - } - - $output .= \sprintf('%s%s%s |%s%s', $prefix, $dumpAsMap ? Inline::dump($key, $flags).':' : '-', '', $blockIndentationIndicator, $blockChompingIndicator); - - foreach (explode("\n", $value) as $row) { - if ('' === $row) { - $output .= "\n"; - } else { - $output .= \sprintf("\n%s%s%s", $prefix, str_repeat(' ', $this->indentation), $row); - } - } + if (Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK & $flags && \is_string($value) && $this->canBeDumpedAsLiteralBlock($value)) { + $output .= \sprintf('%s%s%s |%s%s', $prefix, $dumpAsMap ? Inline::dump($key, $flags).':' : '-', '', $this->getBlockIndentationIndicator($value), $this->getBlockChompingIndicator($value)); + $output .= $this->dumpBlockScalarLines($value, $prefix); continue; } @@ -103,13 +86,9 @@ private function doDump(mixed $input, int $inline = 0, int $indent = 0, int $fla if ($value instanceof TaggedValue) { $output .= \sprintf('%s%s !%s', $prefix, $dumpAsMap ? Inline::dump($key, $flags).':' : '-', $value->getTag()); - if (Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK & $flags && \is_string($value->getValue()) && str_contains($value->getValue(), "\n") && !str_contains($value->getValue(), "\r\n")) { - $blockIndentationIndicator = $this->getBlockIndentationIndicator($value->getValue()); - $output .= \sprintf(' |%s', $blockIndentationIndicator); - - foreach (explode("\n", $value->getValue()) as $row) { - $output .= \sprintf("\n%s%s%s", $prefix, str_repeat(' ', $this->indentation), $row); - } + if (Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK & $flags && \is_string($value->getValue()) && $this->canBeDumpedAsLiteralBlock($value->getValue())) { + $output .= \sprintf(' |%s%s', $this->getBlockIndentationIndicator($value->getValue()), $this->getBlockChompingIndicator($value->getValue())); + $output .= $this->dumpBlockScalarLines($value->getValue(), $prefix); continue; } @@ -148,13 +127,9 @@ private function dumpTaggedValue(TaggedValue $value, int $inline, int $indent, i { $output = \sprintf('%s!%s', $prefix ? $prefix.' ' : '', $value->getTag()); - if (Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK & $flags && \is_string($value->getValue()) && str_contains($value->getValue(), "\n") && !str_contains($value->getValue(), "\r\n")) { - $blockIndentationIndicator = $this->getBlockIndentationIndicator($value->getValue()); - $output .= \sprintf(' |%s', $blockIndentationIndicator); - - foreach (explode("\n", $value->getValue()) as $row) { - $output .= \sprintf("\n%s%s%s", $prefix, str_repeat(' ', $this->indentation), $row); - } + if (Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK & $flags && \is_string($value->getValue()) && $this->canBeDumpedAsLiteralBlock($value->getValue())) { + $output .= \sprintf(' |%s%s', $this->getBlockIndentationIndicator($value->getValue()), $this->getBlockChompingIndicator($value->getValue())); + $output .= $this->dumpBlockScalarLines($value->getValue(), $prefix); return $output; } @@ -166,19 +141,53 @@ private function dumpTaggedValue(TaggedValue $value, int $inline, int $indent, i return $output."\n".$this->doDump($value->getValue(), $inline - 1, $indent, $flags, $nestingLevel + 1); } + private function getBlockChompingIndicator(string $value): string + { + // a block made of line breaks only has no content, so only the keep indicator preserves them + if ("\n" === $value || isset($value[-2]) && "\n" === $value[-2] && "\n" === $value[-1]) { + return '+'; + } + + if ("\n" === $value[-1]) { + return ''; + } + + return '-'; + } + + private function dumpBlockScalarLines(string $value, string $prefix): string + { + $output = ''; + + foreach (explode("\n", $value) as $row) { + if ('' === $row) { + $output .= "\n"; + } else { + $output .= \sprintf("\n%s%s%s", $prefix, str_repeat(' ', $this->indentation), $row); + } + } + + return $output; + } + private function getBlockIndentationIndicator(string $value): string { $lines = explode("\n", $value); - // If the first line (that is neither empty nor contains only spaces) - // starts with a space character, the spec requires a block indentation indicator + // The spec requires a block indentation indicator when the first line that is not empty starts with a space, even if that line holds only spaces // http://www.yaml.org/spec/1.2/spec.html#id2793979 foreach ($lines as $line) { - if ('' !== trim($line, ' ')) { + if ('' !== $line) { return str_starts_with($line, ' ') ? (string) $this->indentation : ''; } } return ''; } + + private function canBeDumpedAsLiteralBlock(string $value): bool + { + // Literal blocks cannot contain invalid UTF-8, non-printable characters or line breaks other than "\n", and YAML 1.1 reads U+0085, U+2028 and U+2029 as line breaks + return str_contains($value, "\n") && preg_match('//u', $value) && !preg_match('/[\x00-\x08\x0b-\x1f\x7f]|\xc2\x85|\xe2\x80[\xa8\xa9]/', $value); + } } diff --git a/upload/api/vendor/symfony/yaml/Escaper.php b/upload/api/vendor/symfony/yaml/Escaper.php index 921d62ff..fcc8334b 100644 --- a/upload/api/vendor/symfony/yaml/Escaper.php +++ b/upload/api/vendor/symfony/yaml/Escaper.php @@ -76,7 +76,7 @@ public static function requiresSingleQuoting(string $value): bool { // Determines if a PHP value is entirely composed of a value that would // require single quoting in YAML. - if (\in_array(strtolower($value), ['null', '~', 'true', 'false', 'y', 'n', 'yes', 'no', 'on', 'off'], true)) { + if (\in_array(strtolower($value), ['null', '~', 'true', 'false', 'y', 'n', 'yes', 'no', 'on', 'off', '.inf', '.nan', '...'], true)) { return true; } diff --git a/upload/api/vendor/symfony/yaml/Exception/ParseException.php b/upload/api/vendor/symfony/yaml/Exception/ParseException.php index 3b96169e..f414429b 100644 --- a/upload/api/vendor/symfony/yaml/Exception/ParseException.php +++ b/upload/api/vendor/symfony/yaml/Exception/ParseException.php @@ -39,7 +39,7 @@ public function __construct( /** * Gets the snippet of code near the error. */ - public function getSnippet(): string + public function getSnippet(): ?string { return $this->snippet; } @@ -59,7 +59,7 @@ public function setSnippet(string $snippet): void * * This method returns null if a string is parsed. */ - public function getParsedFile(): string + public function getParsedFile(): ?string { return $this->parsedFile; } diff --git a/upload/api/vendor/symfony/yaml/Inline.php b/upload/api/vendor/symfony/yaml/Inline.php index 3c24c20f..231ed98b 100644 --- a/upload/api/vendor/symfony/yaml/Inline.php +++ b/upload/api/vendor/symfony/yaml/Inline.php @@ -198,7 +198,7 @@ public static function dump(mixed $value, int $flags = 0, bool $rootLevel = fals $doubleQuoted = Escaper::escapeWithDoubleQuotes($value); return \strlen($doubleQuoted) < \strlen($singleQuoted) ? $doubleQuoted : $singleQuoted; - case Parser::preg_match('{^[0-9]+[_0-9]*$}', $value): + case Parser::preg_match('{^[+-]?[0-9][0-9_]*(\.[0-9_]+)?$}', $value): case Parser::preg_match('{^[+-]?0o[0-7_]++$}', $value): case Parser::preg_match(self::getHexRegex(), $value): case Parser::preg_match(self::getTimestampRegex(), $value): @@ -426,7 +426,9 @@ private static function parseSequence(ParserState $state, string $sequence, int $value = self::parseMapping($state, $sequence, $flags, $i, $references); break; default: - $value = self::parseScalar($sequence, $flags, [',', ']'], $i, null === $tag, $references, $isQuoted, $state); + // a !php/const or !php/enum key of an embedded mapping is evaluated by parseMapping() + $evaluate = null === $tag && !Parser::preg_match('/!php\/(?:const|enum) [^ ,\]]+?(?format('u'), '0')) { - return (float) $time->format('U.u'); + return $time->format('U') + $time->format('u') / 1e6; } try { @@ -888,7 +918,7 @@ private static function parseAnchor(string $value, int &$i): ?string private static function parseTag(string $value, int &$i, int $flags): ?string { - if ('!' !== $value[$i]) { + if (!isset($value[$i]) || '!' !== $value[$i]) { return null; } diff --git a/upload/api/vendor/symfony/yaml/Parser.php b/upload/api/vendor/symfony/yaml/Parser.php index c35b1818..1c94187b 100644 --- a/upload/api/vendor/symfony/yaml/Parser.php +++ b/upload/api/vendor/symfony/yaml/Parser.php @@ -273,12 +273,19 @@ private function doParse(string $value, int $flags): mixed throw new ParseException((is_numeric($key) ? 'Numeric' : 'Non-string').' keys are not supported. Quote your evaluable mapping keys instead.', $this->getRealCurrentLineNb() + 1, $this->currentLine); } + if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && \is_string($key) && str_starts_with($key, "\0")) { + throw new ParseException('Mapping keys starting with a NUL byte are not supported with the "Yaml::PARSE_OBJECT_FOR_MAP" flag.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename); + } + // Convert float keys to strings, to avoid being converted to integers by PHP if (\is_float($key)) { $key = (string) $key; } - if ('<<' === $key && (!isset($values['value']) || '&' !== $values['value'][0] || !self::preg_match('#^&(?P[^ ]+)#u', $values['value'], $refMatches))) { + // Only a plain (unquoted) "<<" key is a merge key + $isMergeKey = '<<' === rtrim($values['key']); + + if ($isMergeKey && (!isset($values['value']) || '&' !== $values['value'][0] || !self::preg_match('#^&(?P[^ ]+)#u', $values['value'], $refMatches))) { $mergeNode = true; $allowOverwrite = true; if (isset($values['value'][0]) && '*' === $values['value'][0]) { @@ -328,7 +335,7 @@ private function doParse(string $value, int $flags): mixed throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename); } - if (isset($parsed[0])) { + if (self::preg_match('/^(?:[ \t]*+(?:#.*+)?\n)*+[ \t]*+(?:\[|-(?:\s|$))/', $value)) { // If the value associated with the merge key is a sequence, then this sequence is expected to contain mapping nodes // and each of these nodes is merged in turn according to its order in the sequence. Keys in mapping nodes earlier // in the sequence override keys specified in later mapping nodes. @@ -338,7 +345,7 @@ private function doParse(string $value, int $flags): mixed } if (!\is_array($parsedItem)) { - throw new ParseException('Merge items must be arrays.', $this->getRealCurrentLineNb() + 1, $parsedItem, $this->filename); + throw new ParseException('Merge items must be arrays.', $this->getRealCurrentLineNb() + 1, \is_scalar($parsedItem) ? $parsedItem : null, $this->filename); } $data += $parsedItem; // array union @@ -349,7 +356,7 @@ private function doParse(string $value, int $flags): mixed $data += $parsed; // array union } } - } elseif ('<<' !== $key && isset($values['value']) && '&' === $values['value'][0] && self::preg_match(self::REFERENCE_PATTERN, $values['value'], $matches)) { + } elseif (!$isMergeKey && isset($values['value']) && '&' === $values['value'][0] && self::preg_match(self::REFERENCE_PATTERN, $values['value'], $matches)) { $isRef = $matches['ref']; $this->refsBeingParsed[] = $isRef; $values['value'] = $matches['value']; @@ -358,7 +365,7 @@ private function doParse(string $value, int $flags): mixed $subTag = null; if ($mergeNode) { // Merge keys - } elseif (!isset($values['value']) || '' === $values['value'] || str_starts_with($values['value'], '#') || (null !== $subTag = $this->getLineTag($values['value'], $flags)) || '<<' === $key) { + } elseif (!isset($values['value']) || '' === $values['value'] || str_starts_with($values['value'], '#') || (null !== $subTag = $this->getLineTag($values['value'], $flags)) || $isMergeKey) { // hash // if next line is less indented or equal, then it means that the current value is null if (!$this->isNextLineIndented() && !$this->isNextLineUnIndentedCollection()) { @@ -381,7 +388,7 @@ private function doParse(string $value, int $flags): mixed // remember the parsed line number here in case we need it to provide some contexts in error messages below $realCurrentLineNbKey = $this->getRealCurrentLineNb(); $value = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(), $flags); - if ('<<' === $key) { + if ($isMergeKey) { $this->refs[$refMatches['ref']] = $value; if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $value instanceof \stdClass) { @@ -905,12 +912,9 @@ private function parseBlockScalar(string $style, string $chomping = '', int $ind $isCurrentLineBlank = $this->isCurrentLineBlank(); $blockLines = []; - $longestBlankLine = 0; // leading blank lines are consumed before determining indentation while ($notEOF && $isCurrentLineBlank && (!$indentation || \strlen($this->currentLine) <= $indentation)) { - $longestBlankLine = max($longestBlankLine, \strlen($this->currentLine)); - // newline only if not EOF if ($notEOF = $this->moveToNextLine()) { $blockLines[] = ''; @@ -925,11 +929,6 @@ private function parseBlockScalar(string $style, string $chomping = '', int $ind for ($i = 0; $i < $currentLineLength && ' ' === $this->currentLine[$i]; ++$i) { ++$indentation; } - - // a comment less indented than the leading empty lines is not content, the indentation is then the one of the longest empty line - if ($indentation < $longestBlankLine && $this->isCurrentLineComment()) { - $indentation = $longestBlankLine; - } } if (!$notEOF || !$indentation || strspn($this->currentLine, ' ') < $indentation) { @@ -1249,22 +1248,26 @@ private function lexInlineQuotedString(int &$cursor = 0): string do { if (++$lineNumber > 1) { - $cursor += strspn($this->currentLine, ' ', $cursor); + $cursor += strspn($this->currentLine, " \t", $cursor); } - if ($this->isCurrentLineBlank()) { + if ($isBlankLine = '' === trim($this->currentLine, " \t")) { $value .= "\n"; } elseif (!$previousLineWasNewline && !$previousLineWasTerminatedWithBackslash) { $value .= ' '; } + $line = ''; + $escapedLength = 0; + for (; \strlen($this->currentLine) > $cursor; ++$cursor) { switch ($this->currentLine[$cursor]) { case '\\': if ("'" === $quotation) { - $value .= '\\'; + $line .= '\\'; } elseif (isset($this->currentLine[++$cursor])) { - $value .= '\\'.$this->currentLine[$cursor]; + $line .= '\\'.$this->currentLine[$cursor]; + $escapedLength = \strlen($line); } break; @@ -1272,17 +1275,17 @@ private function lexInlineQuotedString(int &$cursor = 0): string ++$cursor; if ("'" === $quotation && isset($this->currentLine[$cursor]) && "'" === $this->currentLine[$cursor]) { - $value .= "''"; + $line .= "''"; break; } - return $value.$quotation; + return $value.$line.$quotation; default: - $value .= $this->currentLine[$cursor]; + $line .= $this->currentLine[$cursor]; } } - if ($this->isCurrentLineBlank()) { + if ($isBlankLine) { $previousLineWasNewline = true; $previousLineWasTerminatedWithBackslash = false; } elseif ('"' === $quotation && 1 === (\strlen($this->currentLine) - \strlen(rtrim($this->currentLine, '\\'))) % 2) { @@ -1291,8 +1294,13 @@ private function lexInlineQuotedString(int &$cursor = 0): string } else { $previousLineWasNewline = false; $previousLineWasTerminatedWithBackslash = false; + + // trailing white space is removed when the line is folded, unless it is escaped + $line = substr($line, 0, $escapedLength).rtrim(substr($line, $escapedLength), " \t"); } + $value .= $line; + if ($this->hasMoreLines()) { $cursor = 0; } diff --git a/upload/api/vendor/zircote/swagger-php/README.md b/upload/api/vendor/zircote/swagger-php/README.md index a9d8cb36..cbcda04e 100644 --- a/upload/api/vendor/zircote/swagger-php/README.md +++ b/upload/api/vendor/zircote/swagger-php/README.md @@ -20,7 +20,7 @@ attributes and annotations. - Extracts information from code and existing phpdoc comments. - Can be used programmatically or via command-line tool. - Error reporting (with hints, context). -- 🧪 **Spec attributes (beta)** — attributes you can also build by hand and compile without scanning, and a pipeline that tells you when an attribute is wrong; see [Why spec attributes?](https://zircote.github.io/swagger-php/guide/why-spec). +- 🧪 **Spec attributes (beta)**: attributes that can also be built by hand and compiled without scanning, and a pipeline that reports when an attribute is wrong. See [Why spec attributes?](https://zircote.github.io/swagger-php/guide/why-spec). ## OpenAPI version support @@ -87,6 +87,9 @@ the [examples directory](docs/examples) for more examples. *Available since 6.5.0* +The attribute API is frozen. The beta label stays until v7, because the documents spec mode +generates may still change until then. + A new processing mode using typed attributes from the `OpenApi\Spec` namespace: ```php @@ -109,9 +112,9 @@ $result = (new \OpenApi\Builder()) ->build(); ``` -**Hybrid mode** works with your existing `OpenApi\Attributes` code — no changes needed. It runs the classic scanner +**Hybrid mode** works with your existing `OpenApi\Attributes` code, with no changes needed. It runs the classic scanner but uses the new augmenter pipeline and version-aware compilers, which are easier to extend. -If you'd like to help test the new pipeline, switching to hybrid is the easiest way: +Switching to hybrid is also the easiest way to help test the new pipeline: ```php $result = (new \OpenApi\Builder()) @@ -139,6 +142,9 @@ header('Content-Type: application/x-yaml'); echo $result->toYaml(); ``` +`Builder` is the stable entry point for every mode, and one instance builds one document. +See the [Builder reference](https://zircote.github.io/swagger-php/reference/builder) for its API. + Details on how to generate OpenApi specifications can be found in the [generate reference](https://zircote.github.io/swagger-php/guide/generating-openapi-documents). diff --git a/upload/api/vendor/zircote/swagger-php/ROADMAP.md b/upload/api/vendor/zircote/swagger-php/ROADMAP.md index 0b2f0f65..bf3af1f6 100644 --- a/upload/api/vendor/zircote/swagger-php/ROADMAP.md +++ b/upload/api/vendor/zircote/swagger-php/ROADMAP.md @@ -24,10 +24,15 @@ here: the marker is the promise. `6.5.0` introduced the `Spec` system as opt-in beta. The remainder of **v6** is about completing it and getting people onto it: -* `Spec` attributes declared frozen, and the **beta** label dropped in that minor +* `Spec` attributes declared frozen, and `Builder` declared the stable entry point for every mode. + The **beta** label stays until **v7** * a rector rule set for `OpenApi\Attributes` -> `OpenApi\Spec` * a seam to contribute pre-built attributes to a `Specification` before resolution, for framework integrations that do not scan +* one `component:` field on every reusable `Spec` attribute, naming its key in `components`; + the per-type spellings it replaces (`schema`, `parameter`, `request`, `securityScheme`, and + `response`/`header`/`link`/`example` used as a component key) are marked `@deprecated`, + removed in `8.0`, and trigger a runtime deprecation when used * docblock annotation support marked `@deprecated`, removed in `8.0` - the README has said so since 4.8, the code now agrees, and parsing a docblock annotation triggers a runtime deprecation once per run @@ -35,12 +40,15 @@ it and getting people onto it: `Utils\TypeMapper`, `LegacyTypeResolver`) are removed in `7.0` * `Generator::UNDEFINED` and `Generator::isDefault()` stay until `8.0` - every classic custom processor uses them, so they go with classic and not before +* `Result::log()`, `warnings()` and `errors()` marked `@deprecated`, removed in `8.0` - the logger + set with `Builder::setLogger()` already receives everything they return, and `isValid()` stays Classic attributes, the `Generator` and the processors are **not** marked in v6. The replacement is still beta for part of the major, and a stable API is not deprecated in favour of a beta one. ### v7 This is where things turn: +* the **beta** label comes off the `Spec` attributes and the `hybrid` and `spec` modes * default mode switches to `hybrid` - existing `classic` projects keep working, routed through the new pipeline via the bridge, with the exception of integrations that build annotation objects directly instead of scanning for them, which the bridge has nothing to scan for diff --git a/upload/api/vendor/zircote/swagger-php/bin/openapi b/upload/api/vendor/zircote/swagger-php/bin/openapi index 3d0f5797..f117b717 100755 --- a/upload/api/vendor/zircote/swagger-php/bin/openapi +++ b/upload/api/vendor/zircote/swagger-php/bin/openapi @@ -3,10 +3,12 @@ use OpenApi\Console\GenerateCommand; use OpenApi\Undefined; +use Psr\Log\LogLevel; use Symfony\Component\Console\Application; use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Logger\ConsoleLogger; use Symfony\Component\Console\Output\ConsoleOutput; +use Symfony\Component\Console\Output\OutputInterface; if (class_exists(Undefined::class) === false) { if (file_exists(__DIR__.'/../vendor/autoload.php')) { // cloned / dev environment? @@ -18,7 +20,8 @@ if (class_exists(Undefined::class) === false) { $input = new class extends ArgvInput { - public function hasParameterOption(array|string $values, bool $onlyParams = false): bool + // $values stays untyped: symfony/console 5.4 declares it without a type + public function hasParameterOption($values, bool $onlyParams = false): bool { // Skip the built-in version option check // thus the command can use it for its own purpose @@ -30,7 +33,9 @@ $input = new class extends ArgvInput } }; $output = new ConsoleOutput(); -$logger = new ConsoleLogger($output); +// diagnostics go to stderr: without -o the document itself is written to stdout; +// notices show at normal verbosity, so a default run says what Cleanup removed +$logger = new ConsoleLogger($output->getErrorOutput(), [LogLevel::NOTICE => OutputInterface::VERBOSITY_NORMAL]); $app = new Application(); // Remove Symfony's built-in options that conflict with our command options: diff --git a/upload/api/vendor/zircote/swagger-php/composer.json b/upload/api/vendor/zircote/swagger-php/composer.json index aa6d5c6f..7e6db5ae 100644 --- a/upload/api/vendor/zircote/swagger-php/composer.json +++ b/upload/api/vendor/zircote/swagger-php/composer.json @@ -49,14 +49,15 @@ "phpstan/phpdoc-parser": "^2.0", "psr/log": "^1.1 || ^2.0 || ^3.0", "radebatz/type-info-extras": "^1.0.2", - "symfony/console": "^6.4 || ^7.0 || ^8.0", + "symfony/console": "^5.4 || ^6.4 || ^7.0 || ^8.0", "symfony/deprecation-contracts": "^2 || ^3", "symfony/finder": "^5.0 || ^6.0 || ^7.0 || ^8.0", "symfony/yaml": "^5.4 || ^6.0 || ^7.0 || ^8.0" }, "autoload": { "psr-4": { - "OpenApi\\": "src" + "OpenApi\\": "src", + "OpenApi\\Rector\\": "rector/src" } }, "autoload-dev": { @@ -64,6 +65,7 @@ "/tests/Fixtures" ], "psr-4": { + "OpenApi\\Rector\\Tests\\": "rector/tests/", "OpenApi\\Tools\\": "tools/src/", "OpenApi\\Tools\\Tests\\": "tools/tests/", "OpenApi\\Tests\\": "tests/", @@ -72,13 +74,16 @@ }, "require-dev": { "doctrine/annotations": "^2.0", - "friendsofphp/php-cs-fixer": "3.95.23", - "phpstan/phpstan": "2.2.12", + "friendsofphp/php-cs-fixer": "3.95.27", + "phpstan/phpstan": "2.3.1", "phpunit/phpunit": "^11.5 || >=12.5.22", - "rector/rector": "2.6.5" + "rector/rector": "2.7.0" }, "conflict": { - "symfony/process": ">=6, <6.4.14" + "symfony/console": "<5.4.35", + "symfony/finder": "<5.3.7", + "symfony/process": ">=6, <6.4.14", + "symfony/string": "<5.4.41" }, "scripts-descriptions": { "cs": "Fix all codestyle issues", @@ -100,10 +105,14 @@ "@cs --dry-run", "@rector --dry-run" ], - "test": "export XDEBUG_MODE=off && phpunit --display-all-issues @additional_args", + "test": [ + "export XDEBUG_MODE=off && phpunit --display-all-issues --exclude-testsuite 'Rector Test Suite' @additional_args", + "export XDEBUG_MODE=off && phpunit --display-all-issues --testsuite 'Rector Test Suite' @additional_args" + ], "performance": "export XDEBUG_MODE=off && phpunit --group performance", "analyse": [ - "export XDEBUG_MODE=off && phpstan clear-result-cache -q && phpstan analyse --memory-limit=3G" + "export XDEBUG_MODE=off && phpstan clear-result-cache -q && phpstan analyse --memory-limit=3G", + "export XDEBUG_MODE=off && phpstan analyse -c phpstan-level5.neon.dist --memory-limit=3G" ], "redocly": "npm run redocly -- lint 'docs/examples/specs/**/*.yaml' 'docs/snippets/guide/extension-points/*.yaml' 'tests/Fixtures/Scratch/*.yaml'", "docs:check": "export XDEBUG_MODE=off && phpunit --filter DocsAccuracyTest", diff --git a/upload/api/vendor/zircote/swagger-php/package-lock.json b/upload/api/vendor/zircote/swagger-php/package-lock.json index 98470f89..0ceffc81 100644 --- a/upload/api/vendor/zircote/swagger-php/package-lock.json +++ b/upload/api/vendor/zircote/swagger-php/package-lock.json @@ -9,13 +9,13 @@ "version": "1.0.0", "license": "Apache-2.0", "devDependencies": { - "@redocly/cli": "^2.53.3" + "@redocly/cli": "^2.71.0" } }, "node_modules/@redocly/cli": { - "version": "2.53.3", - "resolved": "https://registry.npmjs.org/@redocly/cli/-/cli-2.53.3.tgz", - "integrity": "sha512-hzNAWzHCOZ05vwRx0ehTxNeJaxxizjGV505eKtfs9MR/8ieD/8lYhAK4GF5FaqRRwOEpLO0PvzQwBSKYXmxhRA==", + "version": "2.71.0", + "resolved": "https://registry.npmjs.org/@redocly/cli/-/cli-2.71.0.tgz", + "integrity": "sha512-QaAo5zdmvtK2GCJzLNZYCn5q3tm2MYImvRt/kDJdwd/zYJsTjNrGl+aindv63tFBrh0FWckXeUnOM5aM+7NQuA==", "dev": true, "license": "MIT", "bin": { diff --git a/upload/api/vendor/zircote/swagger-php/package.json b/upload/api/vendor/zircote/swagger-php/package.json index 74adbc39..239a7148 100644 --- a/upload/api/vendor/zircote/swagger-php/package.json +++ b/upload/api/vendor/zircote/swagger-php/package.json @@ -6,6 +6,6 @@ "redocly": "redocly" }, "devDependencies": { - "@redocly/cli": "^2.53.3" + "@redocly/cli": "^2.71.0" } } diff --git a/upload/api/vendor/zircote/swagger-php/phpstan.neon.dist b/upload/api/vendor/zircote/swagger-php/phpstan.neon.dist deleted file mode 100644 index d03a6e08..00000000 --- a/upload/api/vendor/zircote/swagger-php/phpstan.neon.dist +++ /dev/null @@ -1,36 +0,0 @@ -includes: - - phpstan-baseline.neon - -parameters: - phpVersion: 80200 - level: 5 - paths: - - src - - tests - - tools - - docs/examples - scanDirectories: - # snippets are transcluded into the docs and exercised by tests; known, not analysed - - docs/snippets - scanFiles: - # global namespace on purpose, so not autoloadable and not discovered on its own - - tests/Fixtures/PHP/GlobalNamespaceTypes.php - parallel: - processTimeout: 300.0 - excludePaths: - - 'tests/Fixtures/*' - treatPhpDocTypesAsCertain: false - ignoreErrors: - - - # ignore Generator::UNDEFINED default value - message: '#OpenApi\\Annotations#' - identifier: property.defaultValue - - - # ignore unused example fixtures - message: '#OpenApi\\Examples#' - identifier: property.unused - - - # ignore assigning Generator::UNDEFINED as property value - message: '#Property OpenApi\\Annotations.*::\$.* does not accept string.#' - identifier: assign.propertyType - diff --git a/upload/api/vendor/zircote/swagger-php/rector.php b/upload/api/vendor/zircote/swagger-php/rector.php index 1c4fcc8c..51310a47 100644 --- a/upload/api/vendor/zircote/swagger-php/rector.php +++ b/upload/api/vendor/zircote/swagger-php/rector.php @@ -22,7 +22,6 @@ NewlineAfterStatementRector::class, StringClassNameToClassConstantRector::class => [ __DIR__ . '/src/Analysers/DocBlockParser.php', - __DIR__ . '/tests/Analysers/ComposerAutoloaderScannerTest.php', __DIR__ . '/tests/AnalysisTest.php', __DIR__ . '/tests/ContextTest.php', // provider keys are names the scanner reads out of a file, not classes this diff --git a/upload/api/vendor/zircote/swagger-php/rector/set/classic-to-spec-classes.php b/upload/api/vendor/zircote/swagger-php/rector/set/classic-to-spec-classes.php new file mode 100644 index 00000000..2802d6a3 --- /dev/null +++ b/upload/api/vendor/zircote/swagger-php/rector/set/classic-to-spec-classes.php @@ -0,0 +1,57 @@ + spec `OpenApi\Spec`, for the classes that map one to one. + * Renaming the class is stage one of the migration. Where a spec attribute names or shapes an + * argument differently, the other rules of the set rewrite it. + * + * Two classic attributes are absent because they need a restructure rather than a rename: + * - `Attributes\Query` becomes OpenApi\Spec\Operation with method: 'query' - the HTTP QUERY method is OpenAPI 3.2 and spec has no Operation\Query shortcut yet. + * - `Attributes\Webhook` becomes a webhook: argument on an operation attribute - no longer an attribute of its own. + */ + +return [ + \OpenApi\Attributes\AdditionalProperties::class => \OpenApi\Spec\Schema\AdditionalProperties::class, + \OpenApi\Attributes\Attachable::class => \OpenApi\Spec\Attachable::class, + \OpenApi\Attributes\Components::class => \OpenApi\Spec\Components::class, + \OpenApi\Attributes\Contact::class => \OpenApi\Spec\Contact::class, + \OpenApi\Attributes\CookieParameter::class => \OpenApi\Spec\Parameter\Cookie::class, + \OpenApi\Attributes\Delete::class => \OpenApi\Spec\Operation\Delete::class, + \OpenApi\Attributes\Discriminator::class => \OpenApi\Spec\Discriminator::class, + \OpenApi\Attributes\Encoding::class => \OpenApi\Spec\Encoding::class, + \OpenApi\Attributes\Examples::class => \OpenApi\Spec\Example::class, + \OpenApi\Attributes\ExternalDocumentation::class => \OpenApi\Spec\ExternalDocumentation::class, + \OpenApi\Attributes\Flow::class => \OpenApi\Spec\Flow::class, + \OpenApi\Attributes\Get::class => \OpenApi\Spec\Operation\Get::class, + \OpenApi\Attributes\Head::class => \OpenApi\Spec\Operation\Head::class, + \OpenApi\Attributes\Header::class => \OpenApi\Spec\Header::class, + \OpenApi\Attributes\HeaderParameter::class => \OpenApi\Spec\Parameter\Header::class, + \OpenApi\Attributes\Info::class => \OpenApi\Spec\Info::class, + \OpenApi\Attributes\Items::class => \OpenApi\Spec\Schema\Items::class, + \OpenApi\Attributes\JsonContent::class => \OpenApi\Spec\MediaType\Json::class, + \OpenApi\Attributes\License::class => \OpenApi\Spec\License::class, + \OpenApi\Attributes\Link::class => \OpenApi\Spec\Link::class, + \OpenApi\Attributes\MediaType::class => \OpenApi\Spec\MediaType::class, + \OpenApi\Attributes\OpenApi::class => \OpenApi\Spec\OpenApi::class, + \OpenApi\Attributes\Options::class => \OpenApi\Spec\Operation\Options::class, + \OpenApi\Attributes\Parameter::class => \OpenApi\Spec\Parameter::class, + \OpenApi\Attributes\Patch::class => \OpenApi\Spec\Operation\Patch::class, + \OpenApi\Attributes\PathItem::class => \OpenApi\Spec\PathItem::class, + \OpenApi\Attributes\PathParameter::class => \OpenApi\Spec\Parameter\Path::class, + \OpenApi\Attributes\Post::class => \OpenApi\Spec\Operation\Post::class, + \OpenApi\Attributes\Property::class => \OpenApi\Spec\Property::class, + \OpenApi\Attributes\Put::class => \OpenApi\Spec\Operation\Put::class, + \OpenApi\Attributes\QueryParameter::class => \OpenApi\Spec\Parameter\Query::class, + \OpenApi\Attributes\RequestBody::class => \OpenApi\Spec\RequestBody::class, + \OpenApi\Attributes\Response::class => \OpenApi\Spec\Response::class, + \OpenApi\Attributes\Schema::class => \OpenApi\Spec\Schema::class, + \OpenApi\Attributes\SecurityScheme::class => \OpenApi\Spec\Security\Scheme::class, + \OpenApi\Attributes\Server::class => \OpenApi\Spec\Server::class, + \OpenApi\Attributes\ServerVariable::class => \OpenApi\Spec\ServerVariable::class, + \OpenApi\Attributes\Tag::class => \OpenApi\Spec\Tag::class, + \OpenApi\Attributes\Trace::class => \OpenApi\Spec\Operation\Trace::class, + \OpenApi\Attributes\Xml::class => \OpenApi\Spec\Xml::class, + \OpenApi\Attributes\XmlContent::class => \OpenApi\Spec\MediaType\Xml::class, +]; diff --git a/upload/api/vendor/zircote/swagger-php/rector/set/classic-to-spec.php b/upload/api/vendor/zircote/swagger-php/rector/set/classic-to-spec.php new file mode 100644 index 00000000..8bcc5915 --- /dev/null +++ b/upload/api/vendor/zircote/swagger-php/rector/set/classic-to-spec.php @@ -0,0 +1,50 @@ + `OpenApi\Spec` migration. + * + * vendor/bin/rector process src \ + * --config vendor/zircote/swagger-php/rector/set/classic-to-spec.php --dry-run + * + * What it changes, what it leaves for you, and the generator code it cannot see: + * https://zircote.github.io/swagger-php/guide/migrating-to-spec + * + * This set follows the library's version, carries no backwards-compatibility promise of its + * own, and goes with classic at 8.0. + */ + +use OpenApi\Rector\AliasSpecImportsRector; +use OpenApi\Rector\ComponentsHolderRector; +use OpenApi\Rector\DropParameterArgumentsRector; +use OpenApi\Rector\EncodedPropertyRector; +use OpenApi\Rector\EncodingKeyRector; +use OpenApi\Rector\MoveSchemaKeywordsRector; +use OpenApi\Rector\NamePositionalArgumentsRector; +use OpenApi\Rector\ParameterStyleRector; +use OpenApi\Rector\SecurityToRequirementRector; +use OpenApi\Rector\TypeAliasesRector; +use OpenApi\Rector\TypeClassStringToRefRector; +use OpenApi\Rector\UnnestOpenApiRector; +use OpenApi\Rector\WrapEnumClassStringRector; +use Rector\Config\RectorConfig; +use Rector\Renaming\Rector\Name\RenameClassRector; + +// Order matters where two rules meet one node: positional arguments are named, and the root +// un-nested, while the names are still classic -- before the rename reaches them. +return RectorConfig::configure() + ->withRules([ + NamePositionalArgumentsRector::class, + EncodingKeyRector::class, + EncodedPropertyRector::class, + ParameterStyleRector::class, + DropParameterArgumentsRector::class, + UnnestOpenApiRector::class, + WrapEnumClassStringRector::class, + TypeClassStringToRefRector::class, + TypeAliasesRector::class, + SecurityToRequirementRector::class, + ComponentsHolderRector::class, + ]) + ->withConfiguredRule(RenameClassRector::class, require __DIR__ . '/classic-to-spec-classes.php') + ->withRules([MoveSchemaKeywordsRector::class]) + ->withConfiguredRule(AliasSpecImportsRector::class, [AliasSpecImportsRector::ALIAS => 'OAS']); diff --git a/upload/api/vendor/zircote/swagger-php/rector/src/AliasSpecImportsRector.php b/upload/api/vendor/zircote/swagger-php/rector/src/AliasSpecImportsRector.php new file mode 100644 index 00000000..015793b0 --- /dev/null +++ b/upload/api/vendor/zircote/swagger-php/rector/src/AliasSpecImportsRector.php @@ -0,0 +1,243 @@ +alias = $configuration[self::ALIAS] ?? 'OAS'; + } + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition('Alias fully qualified OpenApi\Spec references and drop the classic import', [new ConfiguredCodeSample( + <<<'CODE_SAMPLE' +use OpenApi\Attributes as OAT; + +#[\OpenApi\Spec\Schema(component: 'Pet')] +class Pet {} +CODE_SAMPLE, + <<<'CODE_SAMPLE' +use OpenApi\Spec as OAS; + +#[OAS\Schema(component: 'Pet')] +class Pet {} +CODE_SAMPLE, + [self::ALIAS => 'OAS'] + )]); + } + + public function getNodeTypes(): array + { + return [Name::class, Namespace_::class]; + } + + public function refactor(Node $node): ?Node + { + if ($node instanceof Name) { + return $this->aliasSpecName($node); + } + + if (!$node instanceof Namespace_) { + return null; + } + + $removed = $this->removeClassicImports($node); + + // A namespace is visited before its children, so on the pass that drops the classic + // import the references below are still classic: nothing spec-shaped is visible yet. + // Dropping the import and adding the alias are therefore one decision, not two. + $added = ($removed || $this->needsAlias($node)) && $this->ensureAliasImport($node); + + return ($removed || $added) ? $node : null; + } + + /** + * `\OpenApi\Spec\Foo\Bar` becomes `\Foo\Bar`. + * + * Returning the replacement rather than mutating in place is what makes Rector reprint it; + * a rule that rewrites grandchildren through its own traverser is silently ignored by the + * printer, which only knows about nodes a rule handed back. + */ + private function aliasSpecName(Name $name): ?Name + { + $prefix = self::SPEC_NAMESPACE . '\\'; + $full = $name->toString(); + + if (!str_starts_with($full, $prefix)) { + return null; + } + + // Name resolution rewrites `OAS\Schema` to its fully qualified form before a rule sees + // it, so an already-aliased reference looks exactly like one that still needs work. + // Without this the rule reports a change on every pass over a migrated file and prints + // the same text back, which Rector flags as a rule applied to no effect. + $original = $name->getAttribute('originalName'); + if ($original instanceof Name && $original->getFirst() === $this->alias) { + return null; + } + + return new Name($this->alias . '\\' . substr($full, strlen($prefix))); + } + + /** + * Drops the imports the alias replaces: classic `OpenApi\Attributes...`, which nothing + * references once the classes are renamed, and any existing `OpenApi\Spec` import -- a + * per-class `use OpenApi\Spec\Schema;` or a bare `use OpenApi\Spec;`. + * + * Clearing the spec ones matters beyond tidiness. A namespace is visited before its + * children, so once they are gone the only spec name left in a `use` is `OpenApi\Spec` + * itself, which the `OpenApi\Spec\` prefix check excludes -- and the name rewriter below + * can no longer corrupt an import into `use OAS\Schema;`. + */ + private function removeClassicImports(Namespace_ $namespace): bool + { + $changed = false; + + foreach ($namespace->stmts as $key => $stmt) { + if (!$stmt instanceof Use_) { + continue; + } + + $keep = []; + foreach ($stmt->uses as $use) { + $name = $use->name->toString(); + + // the alias import itself survives, or the rule would churn on every pass + if ($name === self::SPEC_NAMESPACE && $use->alias?->toString() === $this->alias) { + $keep[] = $use; + + continue; + } + + if ($this->isReplacedImport($name)) { + $changed = true; + + continue; + } + $keep[] = $use; + } + + if (!$keep) { + unset($namespace->stmts[$key]); + + continue; + } + $stmt->uses = $keep; + } + + if ($changed) { + $namespace->stmts = array_values($namespace->stmts); + } + + return $changed; + } + + /** + * True for an import the alias replaces: the classic namespace or a class in it, and any + * spec import other than the alias itself. + */ + private function isReplacedImport(string $name): bool + { + foreach ([self::CLASSIC_NAMESPACE, self::SPEC_NAMESPACE] as $namespace) { + if ($name === $namespace || str_starts_with($name, $namespace . '\\')) { + return true; + } + } + + return false; + } + + private function ensureAliasImport(Namespace_ $namespace): bool + { + + foreach ($namespace->stmts as $stmt) { + if (!$stmt instanceof Use_) { + continue; + } + foreach ($stmt->uses as $use) { + if ($use->name->toString() === self::SPEC_NAMESPACE) { + return false; + } + } + } + + $use = new Use_([new UseItem(new Name(self::SPEC_NAMESPACE), $this->alias)]); + + $at = 0; + foreach ($namespace->stmts as $key => $stmt) { + if ($stmt instanceof Use_) { + $at = $key + 1; + } + } + + array_splice($namespace->stmts, $at, 0, [$use]); + + return true; + } + + /** + * True when the file references the spec namespace, whether or not the references have been + * rewritten yet. A namespace is visited before its children, so on the pass that rewrites + * them this sees the fully qualified form; on any later pass it sees the alias. Checking + * both is what makes the import independent of visit order. + */ + private function needsAlias(Namespace_ $namespace): bool + { + $prefix = self::SPEC_NAMESPACE . '\\'; + + foreach ((new NodeFinder())->findInstanceOf($namespace->stmts, Name::class) as $name) { + if ($name->getFirst() === $this->alias || str_starts_with($name->toString(), $prefix)) { + return true; + } + } + + return false; + } +} diff --git a/upload/api/vendor/zircote/swagger-php/rector/src/ComponentsHolderRector.php b/upload/api/vendor/zircote/swagger-php/rector/src/ComponentsHolderRector.php new file mode 100644 index 00000000..8a5aec55 --- /dev/null +++ b/upload/api/vendor/zircote/swagger-php/rector/src/ComponentsHolderRector.php @@ -0,0 +1,303 @@ + ['schema', ['property', 'parameter', 'header', 'mediaType']], + 'parameter' => ['parameter', ['operation']], + 'requestBody' => ['request', ['operation']], + 'securityScheme' => ['securityScheme', []], + 'response' => ['response', ['operation']], + 'header' => ['header', ['response', 'encoding']], + 'link' => ['link', ['response']], + 'example' => ['example', ['parameter', 'header', 'mediaType']], + ]; + + private const OPERATIONS = ['Get', 'Post', 'Put', 'Patch', 'Delete', 'Head', 'Options', 'Trace', 'Webhook', 'PathItem']; + + /** + * @var list + */ + private array $routeAttributes = []; + + /** + * @param array{routeAttributes?: list} $configuration + */ + public function configure(array $configuration): void + { + $this->routeAttributes = array_map( + static fn (string $class): string => ltrim($class, '\\'), + array_values($configuration[self::ROUTE_ATTRIBUTES] ?? []), + ); + } + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition('Add Components to a class whose members declare keyed components', [new CodeSample( + <<<'CODE_SAMPLE' +class Models +{ + #[\OpenApi\Attributes\Schema(schema: 'Thing', type: 'object')] + public function thing(): void + { + } +} +CODE_SAMPLE, + <<<'CODE_SAMPLE' +#[\OpenApi\Attributes\Components] +class Models +{ + #[\OpenApi\Attributes\Schema(schema: 'Thing', type: 'object')] + public function thing(): void + { + } +} +CODE_SAMPLE + ), new ConfiguredCodeSample( + <<<'CODE_SAMPLE' +class PetController +{ + #[\Symfony\Component\Routing\Attribute\Route('/pets', methods: ['GET'])] + #[\OpenApi\Attributes\Response(response: 200, description: 'The pets')] + public function list(): void + { + } +} +CODE_SAMPLE, + <<<'CODE_SAMPLE' +class PetController +{ + #[\Symfony\Component\Routing\Attribute\Route('/pets', methods: ['GET'])] + #[\OpenApi\Attributes\Response(response: 200, description: 'The pets')] + public function list(): void + { + } +} +CODE_SAMPLE, + [self::ROUTE_ATTRIBUTES => ['Symfony\Component\Routing\Attribute\Route']] + )]); + } + + public function getNodeTypes(): array + { + return [Class_::class]; + } + + public function refactor(Node $node): ?Node + { + if (!$node instanceof Class_) { + return null; + } + + foreach ($this->attributes($node->attrGroups) as [, $local]) { + if ($local === 'Components' || $local === 'Schema') { + return null; + } + } + + $namespace = null; + foreach ($this->memberAttributeGroups($node) as $groups) { + if ($this->isRoute($groups)) { + continue; + } + + $attributes = $this->attributes($groups); + $kinds = array_map(fn (array $attribute): ?string => $this->kind($attribute[1]), $attributes); + + foreach ($attributes as $index => [$attribute, $local, $prefix]) { + $kind = $kinds[$index]; + if ($kind === null || !isset(self::KINDS[$kind]) || !$this->isKeyed($attribute, $kind)) { + continue; + } + + if (array_intersect(self::KINDS[$kind][1], $kinds) === []) { + $namespace = $prefix; + break 2; + } + } + } + + if ($namespace === null) { + return null; + } + + $node->attrGroups[] = new AttributeGroup([new Attribute(new FullyQualified($namespace . 'Components'))]); + + return $node; + } + + /** + * The attribute groups of each member: a property, a constant, a method, or a promoted + * constructor parameter. + * + * @return list> + */ + private function memberAttributeGroups(Class_ $class): array + { + $members = []; + foreach ($class->stmts as $stmt) { + if ($stmt instanceof Property || $stmt instanceof ClassConst) { + $members[] = $stmt->attrGroups; + } elseif ($stmt instanceof ClassMethod) { + if (!$this->isName($stmt, '__construct')) { + $members[] = $stmt->attrGroups; + continue; + } + + foreach ($stmt->params as $param) { + if ($param->flags !== 0) { + $members[] = $param->attrGroups; + } + } + } + } + + return $members; + } + + /** + * Each OpenApi attribute with its class name below the namespace, and the namespace. + * + * @param array $groups + * + * @return list + */ + private function attributes(array $groups): array + { + $attributes = []; + foreach ($groups as $group) { + foreach ($group->attrs as $attribute) { + $name = $this->getName($attribute->name); + foreach (self::NAMESPACES as $namespace) { + if ($name !== null && str_starts_with($name, $namespace)) { + $attributes[] = [$attribute, substr($name, strlen($namespace)), $namespace]; + } + } + } + } + + return $attributes; + } + + /** + * The kind of an attribute, from its class name below the namespace. + */ + private function kind(string $local): ?string + { + return match (true) { + in_array($local, self::OPERATIONS, true), str_starts_with($local, 'Operation\\') => 'operation', + $local === 'Schema' => 'schema', + $local === 'Parameter', str_ends_with($local, 'Parameter'), str_starts_with($local, 'Parameter\\') => 'parameter', + $local === 'RequestBody' => 'requestBody', + $local === 'SecurityScheme', $local === 'Security\\Scheme', str_starts_with($local, 'Security\\Scheme\\') => 'securityScheme', + $local === 'Response' => 'response', + $local === 'Header' => 'header', + $local === 'Link' => 'link', + $local === 'Example' => 'example', + $local === 'Encoding' => 'encoding', + $local === 'Property', str_starts_with($local, 'Property\\') => 'property', + $local === 'MediaType', str_starts_with($local, 'MediaType\\'), $local === 'JsonContent', $local === 'XmlContent' => 'mediaType', + default => null, + }; + } + + /** + * Whether a member carries one of the configured route attributes. + * + * @param array $groups + */ + private function isRoute(array $groups): bool + { + foreach ($groups as $group) { + foreach ($group->attrs as $attribute) { + if (in_array($this->getName($attribute->name), $this->routeAttributes, true)) { + return true; + } + } + } + + return false; + } + + private function isKeyed(Attribute $attribute, string $kind): bool + { + foreach ($attribute->args as $arg) { + if (!$arg instanceof Arg) { + continue; + } + + if ($this->isName($arg, 'component')) { + return true; + } + + if ($this->isName($arg, self::KINDS[$kind][0])) { + return $kind !== 'response' || !$this->isStatusCode($arg->value); + } + } + + return false; + } + + /** + * A literal status code: `200`, `'404'`, `'4XX'` or `'default'`. + */ + private function isStatusCode(Node\Expr $value): bool + { + if ($value instanceof Int_) { + $value = (string) $value->value; + } elseif ($value instanceof String_) { + $value = $value->value; + } else { + return false; + } + + return preg_match(Response::STATUS_CODE_PATTERN, $value) === 1; + } +} diff --git a/upload/api/vendor/zircote/swagger-php/rector/src/DropParameterArgumentsRector.php b/upload/api/vendor/zircote/swagger-php/rector/src/DropParameterArgumentsRector.php new file mode 100644 index 00000000..65ac181d --- /dev/null +++ b/upload/api/vendor/zircote/swagger-php/rector/src/DropParameterArgumentsRector.php @@ -0,0 +1,105 @@ +> + */ + private const DROP = [ + 'OpenApi\Attributes\QueryParameter' => ['in' => 'query'], + 'OpenApi\Attributes\PathParameter' => ['in' => 'path', 'allowEmptyValue' => null, 'allowReserved' => null], + 'OpenApi\Attributes\HeaderParameter' => ['in' => 'header', 'allowEmptyValue' => null, 'allowReserved' => null, 'style' => null, 'spaceDelimited' => null, 'pipeDelimited' => null, 'deepObject' => null], + 'OpenApi\Attributes\CookieParameter' => ['in' => 'cookie', 'allowEmptyValue' => null, 'allowReserved' => null, 'style' => null, 'spaceDelimited' => null, 'pipeDelimited' => null, 'deepObject' => null], + 'OpenApi\Attributes\Header' => ['allowEmptyValue' => null], + ]; + + /** Spec names, valued by the classic name whose list applies. */ + private const SPEC = [ + 'OpenApi\Spec\Parameter\Query' => 'OpenApi\Attributes\QueryParameter', + 'OpenApi\Spec\Parameter\Path' => 'OpenApi\Attributes\PathParameter', + 'OpenApi\Spec\Parameter\Header' => 'OpenApi\Attributes\HeaderParameter', + 'OpenApi\Spec\Parameter\Cookie' => 'OpenApi\Attributes\CookieParameter', + 'OpenApi\Spec\Header' => 'OpenApi\Attributes\Header', + ]; + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition('Drop parameter and header arguments the spec attributes do not take, where nothing is lost', [new CodeSample( + <<<'CODE_SAMPLE' +#[\OpenApi\Attributes\HeaderParameter(name: 'X-Id', in: 'header', style: 'simple', allowEmptyValue: true)] +CODE_SAMPLE, + <<<'CODE_SAMPLE' +#[\OpenApi\Attributes\HeaderParameter(name: 'X-Id')] +CODE_SAMPLE + )]); + } + + public function getNodeTypes(): array + { + return [Attribute::class, New_::class]; + } + + public function refactor(Node $node): ?Node + { + if (!$node instanceof Attribute && !$node instanceof New_) { + return null; + } + + $class = $node instanceof Attribute ? $node->name : $node->class; + $name = $class instanceof Node\Name ? $this->getName($class) : null; + $drop = self::DROP[self::SPEC[$name] ?? $name] ?? null; + if (null === $drop) { + return null; + } + + $args = []; + foreach ($node->args as $arg) { + if ($arg instanceof Arg && $arg->name instanceof Identifier && array_key_exists($arg->name->toString(), $drop)) { + $value = $drop[$arg->name->toString()]; + if (null === $value || ($arg->value instanceof String_ && $arg->value->value === $value)) { + continue; + } + } + + $args[] = $arg; + } + + if (count($args) === count($node->args)) { + return null; + } + + $node->args = $args; + + return $node; + } +} diff --git a/upload/api/vendor/zircote/swagger-php/rector/src/EncodedPropertyRector.php b/upload/api/vendor/zircote/swagger-php/rector/src/EncodedPropertyRector.php new file mode 100644 index 00000000..21978147 --- /dev/null +++ b/upload/api/vendor/zircote/swagger-php/rector/src/EncodedPropertyRector.php @@ -0,0 +1,76 @@ +name : $node->class; + if (!$class instanceof Node\Name || !in_array($this->getName($class), self::CLASSES, true)) { + return null; + } + + foreach ($node->args as $arg) { + if ($arg instanceof Arg && $this->isName($arg, 'encoding')) { + if ($node instanceof Attribute) { + $node->name = new FullyQualified(self::ENCODED); + } else { + $node->class = new FullyQualified(self::ENCODED); + } + + return $node; + } + } + + return null; + } +} diff --git a/upload/api/vendor/zircote/swagger-php/rector/src/EncodingKeyRector.php b/upload/api/vendor/zircote/swagger-php/rector/src/EncodingKeyRector.php new file mode 100644 index 00000000..dbfb6643 --- /dev/null +++ b/upload/api/vendor/zircote/swagger-php/rector/src/EncodingKeyRector.php @@ -0,0 +1,83 @@ +name : $node->class; + if (!$class instanceof Node\Name || !in_array($this->getName($class), self::CLASSES, true)) { + return null; + } + + $key = null; + foreach ($node->args as $arg) { + if (!$arg instanceof Arg) { + continue; + } + + // both given leaves two values to choose from + if ($this->isName($arg, 'encoding')) { + return null; + } + + if ($this->isName($arg, 'property')) { + $key = $arg; + } + } + + if (null === $key) { + return null; + } + + $key->name = new Identifier('encoding'); + + return $node; + } +} diff --git a/upload/api/vendor/zircote/swagger-php/rector/src/MoveSchemaKeywordsRector.php b/upload/api/vendor/zircote/swagger-php/rector/src/MoveSchemaKeywordsRector.php new file mode 100644 index 00000000..f12a5e7c --- /dev/null +++ b/upload/api/vendor/zircote/swagger-php/rector/src/MoveSchemaKeywordsRector.php @@ -0,0 +1,190 @@ + 'OpenApi\Spec\Property', + 'OpenApi\Spec\Property\Encoded' => 'OpenApi\Spec\Property\Encoded', + 'OpenApi\Spec\MediaType\Json' => 'OpenApi\Spec\MediaType\Json', + 'OpenApi\Spec\MediaType\Xml' => 'OpenApi\Spec\MediaType\Xml', + 'OpenApi\Attributes\Property' => 'OpenApi\Spec\Property', + 'OpenApi\Attributes\JsonContent' => 'OpenApi\Spec\MediaType\Json', + 'OpenApi\Attributes\XmlContent' => 'OpenApi\Spec\MediaType\Xml', + ]; + + private const SCHEMA = 'OpenApi\Spec\Schema'; + + /** @var array> */ + private array $accepts = []; + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition('Move schema keywords a spec attribute no longer accepts onto a nested schema', [new CodeSample( + <<<'CODE_SAMPLE' +#[\OpenApi\Spec\Property(property: 'name', type: 'string', nullable: true)] +CODE_SAMPLE, + <<<'CODE_SAMPLE' +#[\OpenApi\Spec\Property(property: 'name', schema: new \OpenApi\Spec\Schema(type: 'string', nullable: true))] +CODE_SAMPLE + )]); + } + + public function getNodeTypes(): array + { + return [Attribute::class, New_::class]; + } + + public function refactor(Node $node): ?Node + { + if (!$node instanceof Attribute && !$node instanceof New_) { + return null; + } + + // an attribute names its class in `name`, a `new` expression in `class` + $class = $node instanceof Attribute ? $node->name : $node->class; + if (!$class instanceof Node\Name) { + return null; + } + + // resolved rather than written: by the time this runs the name may already be aliased + // to `OAS\Property` by AliasSpecImportsRector, and comparing the written form silently + // matches nothing + $name = $this->getName($class); + if (null === $name || !isset(self::TARGETS[$name])) { + return null; + } + + $accepted = $this->accepts(self::TARGETS[$name]); + if (!$accepted) { + return null; + } + + $keep = []; + $move = []; + $ref = null; + foreach ($node->args as $arg) { + // a positional argument cannot be matched to a parameter name here; leave the whole + // attribute alone rather than guess at its meaning + if (!$arg instanceof Arg || !$arg->name instanceof Identifier) { + return null; + } + + $name = $arg->name->toString(); + + if ('schema' === $name) { + // classic takes any string here, and authors wrote a reference: a pointer or a + // class name. The spec argument holds a schema, so it becomes that schema's `ref` + if ($this->isReference($arg->value)) { + $ref = new Arg($arg->value, name: new Identifier('ref')); + + continue; + } + + // an existing schema is the author's own nesting; merging into it is a judgement + // this rule should not make + return null; + } + + if (in_array($name, $accepted, true)) { + $keep[] = $arg; + } else { + $move[] = $arg; + } + } + + if ($ref instanceof Arg) { + // a `ref:` of its own beside the reference leaves two to choose from + foreach ($move as $arg) { + if ($this->isName($arg, 'ref')) { + return null; + } + } + + array_unshift($move, $ref); + } + + if (!$move) { + return null; + } + + $keep[] = new Arg( + new New_(new FullyQualified(self::SCHEMA), $move), + name: new Identifier('schema') + ); + + $node->args = $keep; + + return $node; + } + + /** + * A JSON pointer or a `::class` constant: the two forms a reference takes in a `schema:`. + */ + private function isReference(Node\Expr $value): bool + { + return ($value instanceof Node\Scalar\String_ && str_starts_with($value->value, '#/')) + || ($value instanceof Node\Expr\ClassConstFetch && $this->isName($value->name, 'class')); + } + + /** + * @return list + */ + private function accepts(string $class): array + { + if (isset($this->accepts[$class])) { + return $this->accepts[$class]; + } + + if (!class_exists($class)) { + return $this->accepts[$class] = []; + } + + $constructor = (new \ReflectionClass($class))->getConstructor(); + $names = $constructor + ? array_map(static fn (\ReflectionParameter $p): string => $p->getName(), $constructor->getParameters()) + : []; + + return $this->accepts[$class] = $names; + } +} diff --git a/upload/api/vendor/zircote/swagger-php/rector/src/NamePositionalArgumentsRector.php b/upload/api/vendor/zircote/swagger-php/rector/src/NamePositionalArgumentsRector.php new file mode 100644 index 00000000..2ba308c0 --- /dev/null +++ b/upload/api/vendor/zircote/swagger-php/rector/src/NamePositionalArgumentsRector.php @@ -0,0 +1,215 @@ +> */ + private array $parameters = []; + + public function __construct( + private readonly ReflectionProvider $reflectionProvider, + ) { + } + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition('Name positional arguments to a classic attribute by its classic signature', [new CodeSample( + <<<'CODE_SAMPLE' +class NotFound extends \OpenApi\Attributes\Response +{ + public function __construct() + { + parent::__construct(null, 404, 'Not found'); + } +} +CODE_SAMPLE, + <<<'CODE_SAMPLE' +class NotFound extends \OpenApi\Attributes\Response +{ + public function __construct() + { + parent::__construct(ref: null, response: 404, description: 'Not found'); + } +} +CODE_SAMPLE + )]); + } + + public function getNodeTypes(): array + { + return [Attribute::class, New_::class, Class_::class]; + } + + public function refactor(Node $node): ?Node + { + if ($node instanceof Class_) { + return $this->refactorSubclass($node); + } + + if (!$node instanceof Attribute && !$node instanceof New_) { + return null; + } + + // an attribute names its class in `name`, a `new` expression in `class` + $class = $node instanceof Attribute ? $node->name : $node->class; + if (!$class instanceof Node\Name) { + return null; + } + + $name = $this->getName($class); + if (null === $name || !str_starts_with($name, self::CLASSIC)) { + return null; + } + + return $this->nameArguments($node->args, $name) ? $node : null; + } + + private function refactorSubclass(Class_ $class): ?Class_ + { + if (!$class->extends instanceof Node\Name) { + return null; + } + + $parent = $this->constructorOwner($this->getName($class->extends)); + if (null === $parent || !str_starts_with($parent, self::CLASSIC)) { + return null; + } + + $changed = false; + $this->traverseNodesWithCallable($class->stmts, function (Node $node) use ($parent, &$changed): ?int { + // a nested class has a parent of its own + if ($node instanceof Class_) { + return NodeVisitor::DONT_TRAVERSE_CHILDREN; + } + + if ($node instanceof StaticCall + && $node->class instanceof Node\Name + && 'parent' === $node->class->toLowerString() + && $this->isName($node->name, '__construct') + && $this->nameArguments($node->args, $parent)) { + $changed = true; + } + + return null; + }); + + return $changed ? $class : null; + } + + /** + * The class whose constructor `parent::__construct()` calls: the parent's own, or the one it + * inherits. A user class between the subclass and the attribute only matters if it declares + * a constructor, since then its signature is the one being called. + */ + private function constructorOwner(?string $parent): ?string + { + if (null === $parent || str_starts_with($parent, self::CLASSIC)) { + return $parent; + } + + if (!$this->reflectionProvider->hasClass($parent)) { + return null; + } + + $reflection = $this->reflectionProvider->getClass($parent); + + return $reflection->hasConstructor() + ? $reflection->getConstructor()->getDeclaringClass()->getName() + : null; + } + + /** + * Names every positional argument, or none: a call that cannot be read in full is left as + * the author wrote it rather than half-converted. + * + * @param array $args + */ + private function nameArguments(array $args, string $class): bool + { + $parameters = $this->parameters($class); + + $positional = []; + foreach ($args as $index => $arg) { + if (!$arg instanceof Arg || $arg->unpack) { + return false; + } + + // positional arguments cannot follow named ones, so the first name ends the run + if ($arg->name instanceof Identifier) { + break; + } + + if (!isset($parameters[$index])) { + return false; + } + + $positional[$index] = $arg; + } + + foreach ($positional as $index => $arg) { + $arg->name = new Identifier($parameters[$index]); + } + + return [] !== $positional; + } + + /** + * @return list + */ + private function parameters(string $class): array + { + if (isset($this->parameters[$class])) { + return $this->parameters[$class]; + } + + if (!class_exists($class)) { + return $this->parameters[$class] = []; + } + + $constructor = (new \ReflectionClass($class))->getConstructor(); + + return $this->parameters[$class] = $constructor + ? array_map(static fn (\ReflectionParameter $p): string => $p->getName(), $constructor->getParameters()) + : []; + } +} diff --git a/upload/api/vendor/zircote/swagger-php/rector/src/ParameterStyleRector.php b/upload/api/vendor/zircote/swagger-php/rector/src/ParameterStyleRector.php new file mode 100644 index 00000000..8d75b0cb --- /dev/null +++ b/upload/api/vendor/zircote/swagger-php/rector/src/ParameterStyleRector.php @@ -0,0 +1,107 @@ +name : $node->class; + if (!$class instanceof Node\Name || !in_array($this->getName($class), self::CLASSES, true)) { + return null; + } + + $style = null; + foreach ($node->args as $arg) { + if (!$arg instanceof Arg || !$arg->name instanceof Identifier) { + continue; + } + + $name = $arg->name->toString(); + if ('style' === $name) { + return null; + } + + if (in_array($name, self::STYLES, true)) { + if (null !== $style || $this->isUnset($arg->value)) { + return null; + } + + $style = $arg; + } + } + + if (null === $style) { + return null; + } + + $style->value = new String_($style->name->toString()); + $style->name = new Identifier('style'); + + return $node; + } + + private function isUnset(Node\Expr $value): bool + { + return $value instanceof ConstFetch && ($this->isName($value, 'false') || $this->isName($value, 'null')); + } +} diff --git a/upload/api/vendor/zircote/swagger-php/rector/src/SecurityToRequirementRector.php b/upload/api/vendor/zircote/swagger-php/rector/src/SecurityToRequirementRector.php new file mode 100644 index 00000000..e3ba5501 --- /dev/null +++ b/upload/api/vendor/zircote/swagger-php/rector/src/SecurityToRequirementRector.php @@ -0,0 +1,171 @@ + []]` or the list `['bearerAuth']`. Spec drops a string with a warning, so + * the operation would lose its requirement. It still compiles an array, but deprecates it for a + * `Requirement`. A single scheme becomes `scheme:`, with `scopes:` where it has any, several + * become `schemes:`, and an empty array becomes an empty requirement. + * + * Only literals are rewritten. A variable, a constant or a spread item is left as it is. + */ +final class SecurityToRequirementRector extends AbstractRector +{ + private const NAMESPACES = ['OpenApi\\Attributes\\', 'OpenApi\\Spec\\']; + + private const REQUIREMENT = 'OpenApi\\Spec\\Security\\Requirement'; + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition('Rewrite each item of security: into a Security\Requirement', [new CodeSample( + <<<'CODE_SAMPLE' +#[\OpenApi\Attributes\Get(path: '/users', security: ['bearerAuth', ['oauth2' => ['read']]])] +CODE_SAMPLE, + <<<'CODE_SAMPLE' +#[\OpenApi\Attributes\Get(path: '/users', security: [new \OpenApi\Spec\Security\Requirement(scheme: 'bearerAuth'), new \OpenApi\Spec\Security\Requirement(scheme: 'oauth2', scopes: ['read'])])] +CODE_SAMPLE + )]); + } + + public function getNodeTypes(): array + { + return [Attribute::class, New_::class]; + } + + public function refactor(Node $node): ?Node + { + if (!$node instanceof Attribute && !$node instanceof New_) { + return null; + } + + $class = $node instanceof Attribute ? $node->name : $node->class; + if (!$class instanceof Node\Name || !$this->isOpenApiClass($this->getName($class))) { + return null; + } + + $changed = false; + foreach ($node->args as $arg) { + if (!$arg instanceof Arg || !$this->isName($arg, 'security') || !$arg->value instanceof Array_) { + continue; + } + + foreach ($arg->value->items as $item) { + if ($item === null || $item->key !== null || $item->unpack) { + continue; + } + + $args = $this->requirementArgs($item->value); + if ($args !== null) { + $item->value = new New_(new FullyQualified(self::REQUIREMENT), $args); + $changed = true; + } + } + } + + return $changed ? $node : null; + } + + /** + * The constructor arguments of the `Requirement` an item stands for, or null to leave it. + * + * @return list|null + */ + private function requirementArgs(Expr $value): ?array + { + if ($value instanceof String_) { + return [$this->named('scheme', $value)]; + } + + if (!$value instanceof Array_) { + return null; + } + + $entries = []; + foreach ($value->items as $entry) { + if (!$entry instanceof ArrayItem || $entry->unpack || !$this->isLiteral($entry)) { + return null; + } + $entries[] = $entry; + } + + if ($entries === []) { + return []; + } + + if (count($entries) > 1) { + return [$this->named('schemes', $value)]; + } + + $entry = $entries[0]; + if (!$entry->key instanceof Expr) { + return [$this->named('scheme', $entry->value)]; + } + + $args = [$this->named('scheme', $entry->key)]; + if (!$entry->value instanceof Array_ || $entry->value->items !== []) { + $args[] = $this->named('scopes', $entry->value); + } + + return $args; + } + + /** + * A scheme name is a string, keyed or not, and its scopes are a list of them. + */ + private function isLiteral(ArrayItem $entry): bool + { + if (!$entry->key instanceof Expr) { + return $entry->value instanceof String_; + } + + if (!$entry->key instanceof String_ || !$entry->value instanceof Array_) { + return false; + } + + foreach ($entry->value->items as $scope) { + if (!$scope instanceof ArrayItem || $scope->key instanceof Expr || $scope->unpack || !$scope->value instanceof String_) { + return false; + } + } + + return true; + } + + private function named(string $name, Expr $value): Arg + { + return new Arg($value, name: new Identifier($name)); + } + + private function isOpenApiClass(?string $name): bool + { + foreach (self::NAMESPACES as $namespace) { + if (null !== $name && str_starts_with($name, $namespace)) { + return true; + } + } + + return false; + } +} diff --git a/upload/api/vendor/zircote/swagger-php/rector/src/TypeAliasesRector.php b/upload/api/vendor/zircote/swagger-php/rector/src/TypeAliasesRector.php new file mode 100644 index 00000000..3ca2e1e8 --- /dev/null +++ b/upload/api/vendor/zircote/swagger-php/rector/src/TypeAliasesRector.php @@ -0,0 +1,151 @@ +name : $node->class; + if (!$class instanceof Node\Name || !$this->isOpenApiClass($this->getName($class))) { + return null; + } + + $type = null; + $hasFormat = false; + foreach ($node->args as $arg) { + if (!$arg instanceof Arg) { + continue; + } + + if ($this->isName($arg, 'type')) { + $type = $arg; + } elseif ($this->isName($arg, 'format')) { + $hasFormat = true; + } + } + + if (null === $type) { + return null; + } + + if ($type->value instanceof String_) { + $mapped = $this->lookup($type->value->value); + if (null === $mapped) { + return null; + } + + [$target, $format] = $mapped; + $type->value = new String_($target); + + if (null !== $format && !$hasFormat) { + $node->args[] = new Arg(new String_($format), name: new Identifier('format')); + } + + return $node; + } + + if ($type->value instanceof Array_) { + $changed = false; + foreach ($type->value->items as $item) { + if (!$item instanceof ArrayItem || !$item->value instanceof String_) { + continue; + } + + $mapped = $this->lookup($item->value->value); + if (null !== $mapped) { + $item->value = new String_($mapped[0]); + $changed = true; + } + } + + return $changed ? $node : null; + } + + return null; + } + + /** + * The OpenAPI type and format for a classic alias, or null where the name is not one. + * + * @return array{string, string|null}|null + */ + private function lookup(string $type): ?array + { + $key = strtolower($type); + + if (in_array($key, self::UNMAPPED, true) || !array_key_exists($key, TypeMapper::NATIVE_TYPE_MAP)) { + return null; + } + + $mapped = TypeMapper::NATIVE_TYPE_MAP[$key]; + [$target, $format] = is_array($mapped) ? $mapped : [$mapped, null]; + + return $target === $type ? null : [$target, $format]; + } + + private function isOpenApiClass(?string $name): bool + { + foreach (self::NAMESPACES as $namespace) { + if (null !== $name && str_starts_with($name, $namespace)) { + return true; + } + } + + return false; + } +} diff --git a/upload/api/vendor/zircote/swagger-php/rector/src/TypeClassStringToRefRector.php b/upload/api/vendor/zircote/swagger-php/rector/src/TypeClassStringToRefRector.php new file mode 100644 index 00000000..1f7156d7 --- /dev/null +++ b/upload/api/vendor/zircote/swagger-php/rector/src/TypeClassStringToRefRector.php @@ -0,0 +1,98 @@ +name : $node->class; + if (!$class instanceof Node\Name || !$this->isOpenApiClass($this->getName($class))) { + return null; + } + + $type = null; + foreach ($node->args as $arg) { + if (!$arg instanceof Arg) { + continue; + } + + if ($this->isName($arg, 'ref')) { + return null; + } + + if ($this->isName($arg, 'type') + && $arg->value instanceof ClassConstFetch + && $this->isName($arg->value->name, 'class')) { + $type = $arg; + } + } + + if (null === $type) { + return null; + } + + $type->name = new Identifier('ref'); + + return $node; + } + + private function isOpenApiClass(?string $name): bool + { + foreach (self::NAMESPACES as $namespace) { + if (null !== $name && str_starts_with($name, $namespace)) { + return true; + } + } + + return false; + } +} diff --git a/upload/api/vendor/zircote/swagger-php/rector/src/UnnestOpenApiRector.php b/upload/api/vendor/zircote/swagger-php/rector/src/UnnestOpenApiRector.php new file mode 100644 index 00000000..da065db4 --- /dev/null +++ b/upload/api/vendor/zircote/swagger-php/rector/src/UnnestOpenApiRector.php @@ -0,0 +1,185 @@ + false, 'externalDocs' => false, 'servers' => true, 'tags' => true]; + + /** Arguments spec `OpenApi` still takes. */ + private const KEEP = ['security', 'x', 'attachables']; + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition('Lift the children of a classic root OpenApi attribute into sibling attributes', [new CodeSample( + <<<'CODE_SAMPLE' +#[\OpenApi\Attributes\OpenApi( + openapi: '3.0.0', + info: new \OpenApi\Attributes\Info(title: 'Api', version: '1'), + tags: [new \OpenApi\Attributes\Tag(name: 'public')], +)] +class Api +{ +} +CODE_SAMPLE, + <<<'CODE_SAMPLE' +#[\OpenApi\Attributes\OpenApi(version: '3.0.0'), \OpenApi\Attributes\Info(title: 'Api', version: '1'), \OpenApi\Attributes\Tag(name: 'public')] +class Api +{ +} +CODE_SAMPLE + )]); + } + + public function getNodeTypes(): array + { + return [AttributeGroup::class]; + } + + public function refactor(Node $node): ?Node + { + if (!$node instanceof AttributeGroup) { + return null; + } + + $attrs = []; + $changed = false; + foreach ($node->attrs as $attr) { + $attrs[] = $attr; + + // an empty list still counts: `openapi:` was renamed even if nothing was lifted + $lifted = $this->isName($attr->name, self::OPENAPI) ? $this->lift($attr) : null; + if (null !== $lifted) { + array_push($attrs, ...$lifted); + $changed = true; + } + } + + if (!$changed) { + return null; + } + + $node->attrs = $attrs; + + return $node; + } + + /** + * Strips the liftable arguments off `$openapi` and returns them as attributes, or returns + * null and leaves `$openapi` untouched when there is nothing to do, or when any argument is + * not one this rule can move. + * + * @return list|null + */ + private function lift(Attribute $openapi): ?array + { + $keep = []; + $lifted = []; + $renamed = false; + foreach ($openapi->args as $arg) { + if (!$arg instanceof Arg || !$arg->name instanceof Identifier) { + return null; + } + + $name = $arg->name->toString(); + + if ('openapi' === $name) { + $keep[] = new Arg($arg->value, name: new Identifier('version')); + $renamed = true; + continue; + } + + if (in_array($name, self::KEEP, true)) { + $keep[] = $arg; + continue; + } + + if (!isset(self::LIFT[$name])) { + return null; + } + + $values = self::LIFT[$name] ? $this->items($arg->value) : [$arg->value]; + if (null === $values) { + return null; + } + + foreach ($values as $value) { + if (!$value instanceof New_ || !$value->class instanceof Node\Name) { + return null; + } + + // an attribute takes plain arguments only, so a placeholder leaves the call as written + $args = array_values(array_filter($value->args, static fn (Node $arg): bool => $arg instanceof Arg)); + if (count($args) !== count($value->args)) { + return null; + } + + $lifted[] = new Attribute($value->class, $args); + } + } + + if (!$lifted && !$renamed) { + return null; + } + + $openapi->args = $keep; + + return $lifted; + } + + /** + * @return list|null + */ + private function items(Node\Expr $value): ?array + { + if (!$value instanceof Array_) { + return null; + } + + $items = []; + foreach ($value->items as $item) { + // a keyed or spread entry is not a plain list of attributes + if (null === $item || null !== $item->key || $item->unpack) { + return null; + } + + $items[] = $item->value; + } + + return $items; + } +} diff --git a/upload/api/vendor/zircote/swagger-php/rector/src/WrapEnumClassStringRector.php b/upload/api/vendor/zircote/swagger-php/rector/src/WrapEnumClassStringRector.php new file mode 100644 index 00000000..144c267d --- /dev/null +++ b/upload/api/vendor/zircote/swagger-php/rector/src/WrapEnumClassStringRector.php @@ -0,0 +1,87 @@ +name : $node->class; + if (!$class instanceof Node\Name || !$this->isOpenApiClass($this->getName($class))) { + return null; + } + + foreach ($node->args as $arg) { + if ($arg instanceof Arg + && $this->isName($arg, 'enum') + && $arg->value instanceof ClassConstFetch + && $this->isName($arg->value->name, 'class')) { + $arg->value = new Array_([new ArrayItem($arg->value)]); + + return $node; + } + } + + return null; + } + + private function isOpenApiClass(?string $name): bool + { + foreach (self::NAMESPACES as $namespace) { + if (null !== $name && str_starts_with($name, $namespace)) { + return true; + } + } + + return false; + } +} diff --git a/upload/api/vendor/zircote/swagger-php/src/Analysers/AttributeAnnotationFactory.php b/upload/api/vendor/zircote/swagger-php/src/Analysers/AttributeAnnotationFactory.php index 20bb5cc9..de7005b8 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Analysers/AttributeAnnotationFactory.php +++ b/upload/api/vendor/zircote/swagger-php/src/Analysers/AttributeAnnotationFactory.php @@ -11,6 +11,7 @@ use OpenApi\Context; use OpenApi\Generator; use OpenApi\GeneratorAwareTrait; +use OpenApi\Undefined; class AttributeAnnotationFactory implements AnnotationFactoryInterface { @@ -116,6 +117,15 @@ public function build(\Reflector $reflector, Context $context): array $isParent = static function (OA\AbstractAnnotation $annotation, OA\AbstractAnnotation $possibleParent): bool { // regular annotation hierarchy $explicitParent = null !== $possibleParent->matchNested($annotation) && !$annotation instanceof OA\Attachable; + if ($explicitParent) { + // a single slot already filled has no room; what the root also takes goes there instead, + // as it does for docblock annotations + $slot = $possibleParent::$_nested[$annotation->getRoot()]; + if (!is_array($slot) && !Undefined::isDefault($possibleParent->{$slot}) + && array_key_exists($annotation->getRoot(), OA\OpenApi::$_nested)) { + $explicitParent = false; + } + } $isParentAllowed = false; // support Attachable subclasses if ($isAttachable = $annotation instanceof OA\Attachable) { diff --git a/upload/api/vendor/zircote/swagger-php/src/Analysers/ReflectionAnalyser.php b/upload/api/vendor/zircote/swagger-php/src/Analysers/ReflectionAnalyser.php index f8aa8f2c..0a750d55 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Analysers/ReflectionAnalyser.php +++ b/upload/api/vendor/zircote/swagger-php/src/Analysers/ReflectionAnalyser.php @@ -87,10 +87,22 @@ public function fromFqdn(string $fqdn, Analysis $analysis): Analysis return $analysis; } + if (!is_file($filename)) { + $analysis->context?->logger?->warning("Skipping {$fqdn}: its source file {$filename} cannot be read"); + + return $analysis; + } + $scanner = new TokenScanner(); $fileDetails = $scanner->scanFile($filename); - $this->analyzeFqdn($fqdn, $analysis, $fileDetails[$fqdn]); + // the scanner keys a class in the global namespace with its leading backslash + $details = $fileDetails[$fqdn] ?? $fileDetails['\\' . $fqdn] ?? null; + if ($details === null) { + return $analysis; + } + + $this->analyzeFqdn($fqdn, $analysis, $details); return $analysis; } diff --git a/upload/api/vendor/zircote/swagger-php/src/Annotations/AbstractAnnotation.php b/upload/api/vendor/zircote/swagger-php/src/Annotations/AbstractAnnotation.php index ee94f6df..8d2a6cdf 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Annotations/AbstractAnnotation.php +++ b/upload/api/vendor/zircote/swagger-php/src/Annotations/AbstractAnnotation.php @@ -19,6 +19,11 @@ */ abstract class AbstractAnnotation implements \JsonSerializable { + /** + * A 3.0 schema that matches only `null`: 3.0 has no `null` type, and `nullable` only takes effect beside a `type`. + */ + protected const NULL_SCHEMA_30 = ['type' => 'object', 'nullable' => true, 'enum' => [null]]; + /** * While the OpenAPI Specification tries to accommodate most use cases, additional data can be added to extend the specification at certain points. * For further details see https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#specificationExtensions @@ -329,19 +334,28 @@ public function jsonSerialize(): \stdClass } } } - if (property_exists($this, 'nullable') && $this->nullable === true) { - $ref = ['oneOf' => [$ref]]; - if (!$this->_context->isVersion('3.0.x')) { - $ref['oneOf'][] = ['type' => 'null']; - } else { - $ref['nullable'] = $data->nullable; + $nullable = property_exists($this, 'nullable') && $this->nullable === true; + // a Schema's $ref is a JSON Schema keyword in 3.1, so its annotations stay beside it + if (!$nullable && $this instanceof Schema && !$this->_context->isVersion('3.0.x')) { + foreach (['title', 'default', 'example', 'examples', 'deprecated', 'readOnly', 'writeOnly'] as $prop) { + if (property_exists($data, $prop)) { + $ref[$prop] = $data->{$prop}; + } } + } + if ($nullable) { + $ref = ['oneOf' => [$ref]]; + $ref['oneOf'][] = $this->_context->isVersion('3.0.x') ? self::NULL_SCHEMA_30 : ['type' => 'null']; unset($data->ref, $data->nullable); // preserve other properties foreach (get_object_vars($data) as $property => $value) { $ref[$property] = $value; } + // an explicit type on the wrapper has to let null through too + if (isset($ref['type']) && $this->_context->isVersion('3.0.x')) { + $ref['nullable'] = true; + } } $data = (object) $ref; } @@ -375,6 +389,19 @@ public function jsonSerialize(): \stdClass } unset($data->unevaluatedProperties); + + // without a type beside it, nullable has no effect; null gets a branch of its own where there is a choice + if (isset($data->nullable) && true === $data->nullable && !isset($data->type)) { + if (isset($data->oneOf)) { + $data->oneOf[] = self::NULL_SCHEMA_30; + unset($data->nullable); + } elseif (isset($data->anyOf)) { + $data->anyOf[] = self::NULL_SCHEMA_30; + unset($data->nullable); + } elseif (!isset($data->allOf)) { + unset($data->nullable); + } + } } if (!$this->_context->isVersion('3.0.x')) { diff --git a/upload/api/vendor/zircote/swagger-php/src/Annotations/JsonSchemaTrait.php b/upload/api/vendor/zircote/swagger-php/src/Annotations/JsonSchemaTrait.php index 953d0ac3..a4e6b8eb 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Annotations/JsonSchemaTrait.php +++ b/upload/api/vendor/zircote/swagger-php/src/Annotations/JsonSchemaTrait.php @@ -160,7 +160,7 @@ trait JsonSchemaTrait public $patternProperties = Undefined::UNDEFINED; /** - * @var array + * @var array|bool */ public $unevaluatedProperties = Undefined::UNDEFINED; @@ -260,7 +260,7 @@ trait JsonSchemaTrait int|null $maxContains = null, array|null $prefixItems = null, array|null $patternProperties = null, - array|null $unevaluatedProperties = null, + bool|array|null $unevaluatedProperties = null, mixed $unevaluatedItems = Undefined::UNDEFINED, mixed $dependencies = Undefined::UNDEFINED, array|null $dependentRequired = null, diff --git a/upload/api/vendor/zircote/swagger-php/src/Annotations/OpenApi.php b/upload/api/vendor/zircote/swagger-php/src/Annotations/OpenApi.php index 55fcf9a6..c31f9db1 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Annotations/OpenApi.php +++ b/upload/api/vendor/zircote/swagger-php/src/Annotations/OpenApi.php @@ -9,6 +9,7 @@ use OpenApi\Analysis; use OpenApi\OpenApiException; use OpenApi\Undefined; +use OpenApi\Utils\SecurityRequirement; /** * This is the root document object for the API specification. @@ -247,6 +248,13 @@ public function jsonSerialize(): \stdClass unset($data->tags); } + // ensure security elements are object + if (isset($data->security) && is_array($data->security)) { + foreach ($data->security as $key => $scheme) { + $data->security[$key] = (object) SecurityRequirement::normalise((array) $scheme); + } + } + return $data; } diff --git a/upload/api/vendor/zircote/swagger-php/src/Annotations/Operation.php b/upload/api/vendor/zircote/swagger-php/src/Annotations/Operation.php index f0c69c0f..8c51f838 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Annotations/Operation.php +++ b/upload/api/vendor/zircote/swagger-php/src/Annotations/Operation.php @@ -9,6 +9,7 @@ use OpenApi\Analysis; use OpenApi\Annotations as OA; use OpenApi\Undefined; +use OpenApi\Utils\SecurityRequirement; /** * Base class for @OA\Get, @OA\Post, @OA\Put, etc. @@ -201,7 +202,7 @@ public function jsonSerialize(): \stdClass // ensure security elements are object if (isset($data->security) && is_array($data->security)) { foreach ($data->security as $key => $scheme) { - $data->security[$key] = (object) $scheme; + $data->security[$key] = (object) SecurityRequirement::normalise((array) $scheme); } } diff --git a/upload/api/vendor/zircote/swagger-php/src/Assembler.php b/upload/api/vendor/zircote/swagger-php/src/Assembler.php index 667d4ed7..b727f1aa 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Assembler.php +++ b/upload/api/vendor/zircote/swagger-php/src/Assembler.php @@ -84,7 +84,7 @@ protected function resolveReflector(\ReflectionClass|\ReflectionMethod|\Reflecti return []; } - $resolved = $this->attributeFactory->resolveHierarchy($outer, $inner); + $resolved = $this->attributeFactory->withoutStrayComponents($outer, $this->attributeFactory->resolveHierarchy($outer, $inner)); $roots = []; foreach ($resolved as $attribute) { diff --git a/upload/api/vendor/zircote/swagger-php/src/Assembler/OperationMethodsTranslator.php b/upload/api/vendor/zircote/swagger-php/src/Assembler/OperationMethodsTranslator.php new file mode 100644 index 00000000..6ded95d4 --- /dev/null +++ b/upload/api/vendor/zircote/swagger-php/src/Assembler/OperationMethodsTranslator.php @@ -0,0 +1,175 @@ + + */ + protected array $split = []; + + public function reset(): void + { + $this->split = []; + } + + /** + * @param array $attributes current attributes; a previous translator's + * output, so not yet narrowed to AttributeInterface + * @param array $created newly created attribute instances + * @param AttributeReflector $reflector + * @return array attributes to carry forward; AttributeFactory filters + * the final result down to AttributeInterface + */ + public function translate(array $attributes, array $created, \ReflectionClass|\ReflectionMethod|\ReflectionProperty|\ReflectionParameter|\ReflectionClassConstant $reflector): array + { + $translated = [...$attributes, ...$created]; + + // a method's parameters are read after the method, as reflectors of their own; what sits + // on them nests into the split operations through contained(), so it fans out too + if ($reflector instanceof \ReflectionParameter) { + $function = $reflector->getDeclaringFunction(); + if ($function instanceof \ReflectionMethod && isset($this->split[$this->key($function)])) { + $this->fanOut($translated, 'contained'); + } + + return $translated; + } + + // counted before the split: a stacked operation beside the split one leaves fan alone + $operations = array_filter($translated, static fn (object $attribute): bool => $attribute instanceof OA\Operation); + + $result = []; + $fanned = false; + foreach ($translated as $attribute) { + if (!$attribute instanceof OA\Operation || $attribute->methods === null) { + $result[] = $attribute; + continue; + } + + // in place, so the copies keep the declared operation's position among its siblings + $split = $this->split($attribute); + array_push($result, ...$split); + $fanned = $fanned || (count($split) > 1 && count($operations) === 1); + } + + if ($fanned) { + // siblings nest through merge(), after every translator has run + $this->fanOut($result, 'merge'); + + // an invokable class declares its handler's operation on the class + $handler = $reflector instanceof \ReflectionClass && $reflector->hasMethod('__invoke') + ? $reflector->getMethod('__invoke') + : $reflector; + if ($handler instanceof \ReflectionMethod) { + $this->split[$this->key($handler)] = true; + } + } + + return $result; + } + + /** + * @return list + */ + protected function split(OA\Operation $operation): array + { + $methods = $operation->methods ?? []; + + if ($operation->method !== null) { + throw OpenApiException::fromSource( + sprintf('%s declares both `method` and `methods`, keep one of them', $operation::class), + $operation->getSourceLocation(), + ); + } + + // the copies are plain single-method operations from here on + $operation->methods = null; + + if (count($methods) > 1 && $operation->operationId !== null && !str_contains($operation->operationId, '{method}')) { + $this->logger?->warning(sprintf( + 'operationId "%s" ignored on an operation with several methods (%s), add {method} to tell them apart in %s', + $operation->operationId, + implode(', ', $methods), + $operation->getSourceLocation(), + )); + $operation->operationId = null; + } + + // the declared operation takes the first method, so what already holds it, a meta key + // or the reflector, still finds it; copy() is deep, so no child is shared between methods + $split = []; + foreach ($methods as $position => $method) { + $copy = $position === 0 ? $operation : $operation->copy(); + $copy->method = $method; + $split[] = $copy; + } + + // `methods: []` names none: left without a method, for the compiler to report + return $split === [] ? [$operation] : $split; + } + + /** + * Set `Fan::All` on every attribute that nests into an operation by `$targets` and leaves + * `fan` unset. + * + * @param array $attributes + * @param 'merge'|'contained' $targets + */ + protected function fanOut(array $attributes, string $targets): void + { + foreach ($attributes as $attribute) { + // an explicit fan, Single included, is the author's and stays + if (!$attribute instanceof AttributeInterface || $attribute instanceof OA\Operation || $attribute->getFan() instanceof OA\Fan) { + continue; + } + + foreach (array_keys($attribute->{$targets}()) as $type) { + // a target naming Operation or one of its parents takes the split operations + if (is_a(OA\Operation::class, $type, true)) { + $attribute->setFan(OA\Fan::All); + break; + } + } + } + } + + protected function key(\ReflectionMethod $method): string + { + return $method->getDeclaringClass()->getName() . '::' . $method->getName(); + } +} diff --git a/upload/api/vendor/zircote/swagger-php/src/Assembler/OptionalPropertyAttributeTranslator.php b/upload/api/vendor/zircote/swagger-php/src/Assembler/OptionalPropertyAttributeTranslator.php index d5dd78a9..c397848e 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Assembler/OptionalPropertyAttributeTranslator.php +++ b/upload/api/vendor/zircote/swagger-php/src/Assembler/OptionalPropertyAttributeTranslator.php @@ -18,6 +18,9 @@ * This is what implements the implicit `OA\Property` shortcut. The `OA\MediaType` shortcuts * are the `Augmenter\Shortcuts` augmenter; the `OA\Parameter` ones are plain subclasses. * + * Not in a class carrying `OA\Components`: there a schema on a property is a component of its + * own, named after the property, so it is left for the holder to absorb. + * * @phpstan-import-type AttributeReflector from AttributeTranslatorInterface */ class OptionalPropertyAttributeTranslator extends AbstractAttributeTranslator @@ -44,8 +47,9 @@ public function translate(array $attributes, array $created, \ReflectionClass|\R $hasSchema = $hasInstance($translated, OA\Schema::class); $hasEncoding = $hasInstance($translated, OA\Encoding::class); - if ($reflector instanceof \ReflectionProperty - || ($reflector instanceof \ReflectionParameter && $reflector->getDeclaringFunction()->getName() === '__construct') + if (($reflector instanceof \ReflectionProperty + || ($reflector instanceof \ReflectionParameter && $reflector->getDeclaringFunction()->getName() === '__construct')) + && $reflector->getDeclaringClass()?->getAttributes(OA\Components::class) === [] ) { $hasProperty = $hasInstance($translated, OA\Property::class); diff --git a/upload/api/vendor/zircote/swagger-php/src/Attributes/AdditionalProperties.php b/upload/api/vendor/zircote/swagger-php/src/Attributes/AdditionalProperties.php index bfae5653..a37e4f01 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Attributes/AdditionalProperties.php +++ b/upload/api/vendor/zircote/swagger-php/src/Attributes/AdditionalProperties.php @@ -78,7 +78,7 @@ public function __construct( int|null $maxContains = null, array|null $prefixItems = null, array|null $patternProperties = null, - array|null $unevaluatedProperties = null, + bool|array|null $unevaluatedProperties = null, mixed $unevaluatedItems = Undefined::UNDEFINED, mixed $dependencies = Undefined::UNDEFINED, array|null $dependentRequired = null, diff --git a/upload/api/vendor/zircote/swagger-php/src/Attributes/Items.php b/upload/api/vendor/zircote/swagger-php/src/Attributes/Items.php index c89a3cdf..2e2f056e 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Attributes/Items.php +++ b/upload/api/vendor/zircote/swagger-php/src/Attributes/Items.php @@ -78,7 +78,7 @@ public function __construct( int|null $maxContains = null, array|null $prefixItems = null, array|null $patternProperties = null, - array|null $unevaluatedProperties = null, + bool|array|null $unevaluatedProperties = null, mixed $unevaluatedItems = Undefined::UNDEFINED, mixed $dependencies = Undefined::UNDEFINED, array|null $dependentRequired = null, diff --git a/upload/api/vendor/zircote/swagger-php/src/Attributes/JsonContent.php b/upload/api/vendor/zircote/swagger-php/src/Attributes/JsonContent.php index 85139c8f..d6a8d049 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Attributes/JsonContent.php +++ b/upload/api/vendor/zircote/swagger-php/src/Attributes/JsonContent.php @@ -100,7 +100,7 @@ public function __construct( int|null $maxContains = null, array|null $prefixItems = null, array|null $patternProperties = null, - array|null $unevaluatedProperties = null, + bool|array|null $unevaluatedProperties = null, mixed $unevaluatedItems = Undefined::UNDEFINED, mixed $dependencies = Undefined::UNDEFINED, array|null $dependentRequired = null, diff --git a/upload/api/vendor/zircote/swagger-php/src/Attributes/Property.php b/upload/api/vendor/zircote/swagger-php/src/Attributes/Property.php index d92e207a..1e81cf3d 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Attributes/Property.php +++ b/upload/api/vendor/zircote/swagger-php/src/Attributes/Property.php @@ -81,7 +81,7 @@ public function __construct( int|null $maxContains = null, array|null $prefixItems = null, array|null $patternProperties = null, - array|null $unevaluatedProperties = null, + bool|array|null $unevaluatedProperties = null, mixed $unevaluatedItems = Undefined::UNDEFINED, mixed $dependencies = Undefined::UNDEFINED, array|null $dependentRequired = null, diff --git a/upload/api/vendor/zircote/swagger-php/src/Attributes/Schema.php b/upload/api/vendor/zircote/swagger-php/src/Attributes/Schema.php index a19e626f..5a606221 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Attributes/Schema.php +++ b/upload/api/vendor/zircote/swagger-php/src/Attributes/Schema.php @@ -78,7 +78,7 @@ public function __construct( int|null $maxContains = null, array|null $prefixItems = null, array|null $patternProperties = null, - array|null $unevaluatedProperties = null, + bool|array|null $unevaluatedProperties = null, mixed $unevaluatedItems = Undefined::UNDEFINED, mixed $dependencies = Undefined::UNDEFINED, array|null $dependentRequired = null, diff --git a/upload/api/vendor/zircote/swagger-php/src/Attributes/XmlContent.php b/upload/api/vendor/zircote/swagger-php/src/Attributes/XmlContent.php index 1df11ab1..2dd0ae7a 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Attributes/XmlContent.php +++ b/upload/api/vendor/zircote/swagger-php/src/Attributes/XmlContent.php @@ -81,7 +81,7 @@ public function __construct( int|null $maxContains = null, array|null $prefixItems = null, array|null $patternProperties = null, - array|null $unevaluatedProperties = null, + bool|array|null $unevaluatedProperties = null, mixed $unevaluatedItems = Undefined::UNDEFINED, mixed $dependencies = Undefined::UNDEFINED, array|null $dependentRequired = null, diff --git a/upload/api/vendor/zircote/swagger-php/src/Augmenter/Cleanup.php b/upload/api/vendor/zircote/swagger-php/src/Augmenter/Cleanup.php index b75c6d57..a7640921 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Augmenter/Cleanup.php +++ b/upload/api/vendor/zircote/swagger-php/src/Augmenter/Cleanup.php @@ -22,8 +22,10 @@ * Iterates multiple times to catch nested dependencies (a schema only * referenced by another unused schema should also be removed). * - * Removal is silent, with one exception: a response component keyed by a status code is - * reported, because it is a response that was meant to nest into an operation. + * Removal is reported once per run, as a notice with the count and the switch that turns it + * off; the components themselves are listed at debug level. A response component keyed by a + * status code is also reported on its own, as a warning, because it is a response that was + * meant to nest into an operation. * * @implements PipeInterface */ @@ -33,6 +35,9 @@ class Cleanup implements PipeInterface, LoggerAwareInterface protected const MAX_ITERATIONS = 10; + /** @var list references of the components removed so far, in removal order */ + protected array $removed = []; + public function __construct( #[Config('Enables/disables removal of unreferenced components.')] protected bool $enabled = true, @@ -45,12 +50,17 @@ public function __invoke(mixed $payload): mixed return null; } + $this->removed = []; + for ($i = 0; $i < self::MAX_ITERATIONS; ++$i) { if (!$this->cleanup($payload)) { + $this->reportRemoved(); + return null; } } + $this->reportRemoved(); $this->logger?->warning('CleanUnused: maximum iterations ({max}) reached, some unused components may remain', [ 'max' => self::MAX_ITERATIONS, ]); @@ -101,8 +111,10 @@ protected function removeUnused(Specification $specification, string $bucket, ar $removed = false; foreach ($specification->{$bucket} as $index => $item) { $name = ComponentName::of($item); - if ($name !== null && !isset($usedRefs[JsonPointer::ref('components', $bucket, $name)])) { + $ref = $name !== null ? JsonPointer::ref('components', $bucket, $name) : null; + if ($ref !== null && !isset($usedRefs[$ref])) { $this->reportStatusCodeKey($item, $name); + $this->removed[] = $ref; unset($specification->{$bucket}[$index]); $removed = true; } @@ -114,6 +126,25 @@ protected function removeUnused(Specification $specification, string $bucket, ar return $removed; } + protected function reportRemoved(): void + { + if ($this->removed === []) { + return; + } + + $count = count($this->removed); + $this->logger?->notice(sprintf( + 'Removed %d unreferenced %s; set cleanup.enabled to false to keep %s', + $count, + $count === 1 ? 'component' : 'components', + $count === 1 ? 'it' : 'them', + )); + + foreach ($this->removed as $ref) { + $this->logger?->debug(sprintf('Removed unreferenced component %s', $ref)); + } + } + /** * `Response::$response` is the status code when the response is nested in an operation * and the component name when it is not, and `isRoot()` cannot tell the two apart — it diff --git a/upload/api/vendor/zircote/swagger-php/src/Augmenter/DefaultMediaType.php b/upload/api/vendor/zircote/swagger-php/src/Augmenter/DefaultMediaType.php new file mode 100644 index 00000000..211f43f4 --- /dev/null +++ b/upload/api/vendor/zircote/swagger-php/src/Augmenter/DefaultMediaType.php @@ -0,0 +1,51 @@ + + */ +class DefaultMediaType implements PipeInterface +{ + public function __construct( + #[Config('The media type given to content that names none.')] + protected string $mediaType = 'application/json', + ) { + } + + public function __invoke(mixed $payload): mixed + { + $payload->getWalker()->visit(OA\MediaType::class, function (OA\MediaType $mediaType): void { + $mediaType->mediaType ??= $this->mediaType; + }); + + return null; + } + + public function setMediaType(string $mediaType): static + { + $this->mediaType = $mediaType; + + return $this; + } + + public function group(): string|\BackedEnum + { + return Group::Resolve; + } +} diff --git a/upload/api/vendor/zircote/swagger-php/src/Augmenter/Docblocks.php b/upload/api/vendor/zircote/swagger-php/src/Augmenter/Docblocks.php index 876b235b..bbf53231 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Augmenter/Docblocks.php +++ b/upload/api/vendor/zircote/swagger-php/src/Augmenter/Docblocks.php @@ -9,6 +9,7 @@ use OpenApi\Contracts\AttributeInterface; use OpenApi\Spec as OA; use OpenApi\Specification; +use OpenApi\Type\TypeMapper; use OpenApi\Undefined; use OpenApi\Utils\DocBlockParser; use OpenApi\Utils\PipeInterface; @@ -237,7 +238,7 @@ protected function augmentProperties(OA\Schema $schema): void } if (Undefined::isDefault($property->schema->example)) { - $property->schema->example = $this->parser->extractExample($docblock) ?? Undefined::UNDEFINED; + $property->schema->example = $this->extractExample($property->schema, $docblock); } } } @@ -280,4 +281,47 @@ protected function extractDescription(string $content): ?string return Undefined::isDefault($description) ? null : $description; } + + protected function extractExample(OA\Schema $schema, string $docblock): mixed + { + $example = $this->parser->extractExample($docblock); + if ($example === null) { + return Undefined::UNDEFINED; + } + + $types = (array) $schema->type; + if ($types === [] || in_array('string', $types, true)) { + return $example; + } + + try { + $value = json_decode($example, flags: JSON_THROW_ON_ERROR | JSON_BIGINT_AS_STRING); + $decoded = json_decode($example, flags: JSON_THROW_ON_ERROR); + + /* + * Reject non-finite numbers and integers that would become strings, including nested values. + */ + if (json_encode($value, JSON_THROW_ON_ERROR) !== json_encode($decoded, JSON_THROW_ON_ERROR)) { + return $example; + } + } catch (\JsonException) { + return $example; + } + + $valueType = match (true) { + $value === null => 'null', + $value instanceof \stdClass => 'object', + is_float($value) && floor($value) === $value => 'integer', + default => TypeMapper::valueType($value), + }; + + if (in_array($valueType, $types, true) + || ($valueType === 'integer' && in_array('number', $types, true)) + || ($valueType === 'null' && $schema->nullable === true) + ) { + return $value; + } + + return $example; + } } diff --git a/upload/api/vendor/zircote/swagger-php/src/Augmenter/Enums.php b/upload/api/vendor/zircote/swagger-php/src/Augmenter/Enums.php index 3a2ef305..10dfdebf 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Augmenter/Enums.php +++ b/upload/api/vendor/zircote/swagger-php/src/Augmenter/Enums.php @@ -8,6 +8,7 @@ use OpenApi\Spec as OA; use OpenApi\Specification; +use OpenApi\Type\TypeMapper; use OpenApi\Utils\Config; use OpenApi\Utils\PipeInterface; use OpenApi\Utils\ServerVariableEnum; @@ -72,7 +73,7 @@ protected function expandEnumSchemas(Specification $specification): void $enumName = $reflector->getName(); $reflector = new \ReflectionEnum($enumName); - $schema->schema ??= $reflector->getShortName(); + $schema->component ??= $reflector->getShortName(); $useName = $this->shouldUseName($schema, $reflector); @@ -148,10 +149,10 @@ protected function resolveEnumArray(array $enum): array $resolved = []; foreach ($enum as $value) { if ($value instanceof \UnitEnum) { - $resolved[] = $value instanceof \BackedEnum ? $value->value : $value->name; + $resolved[] = TypeMapper::caseValue($value); } elseif (is_string($value) && function_exists('enum_exists') && enum_exists($value)) { foreach ($value::cases() as $case) { - $resolved[] = $case instanceof \BackedEnum ? $case->value : $case->name; + $resolved[] = TypeMapper::caseValue($case); } } else { $resolved[] = $value; diff --git a/upload/api/vendor/zircote/swagger-php/src/Augmenter/ImpliedSchemas.php b/upload/api/vendor/zircote/swagger-php/src/Augmenter/ImpliedSchemas.php new file mode 100644 index 00000000..1ca173f5 --- /dev/null +++ b/upload/api/vendor/zircote/swagger-php/src/Augmenter/ImpliedSchemas.php @@ -0,0 +1,120 @@ + + */ +class ImpliedSchemas implements PipeInterface +{ + /** @var array, true> */ + protected array $enums = []; + + public function __construct( + protected TypeResolver $typeResolver = new TypeResolver(), + ) { + } + + public function __invoke(mixed $payload): mixed + { + $this->enums = []; + + $walker = $payload->getWalker(); + $walker->visit(OA\Property::class, function (OA\Property $property): void { + $this->collect($property->getReflector(), $property->schema); + }); + $walker->visit(OA\Parameter::class, function (OA\Parameter $parameter): void { + $this->collect($parameter->getReflector(), $parameter->schema); + }); + $walker->visit(OA\RequestBody::class, function (OA\RequestBody $requestBody): void { + $this->collect($requestBody->getReflector()); + }); + + $index = $payload->buildComponentIndex(); + foreach (array_keys($this->enums) as $enum) { + if ($index->find($enum) instanceof AttributeInterface) { + continue; + } + + $reflector = new \ReflectionEnum($enum); + $type = match ($reflector->getBackingType()?->getName()) { + 'int' => 'integer', + 'string' => 'string', + default => null, + }; + $schema = new OA\Schema(type: $type); + $schema->setReflector($reflector); + $payload->add($schema); + } + + return null; + } + + public function setTypeResolver(TypeResolver $typeResolver): static + { + $this->typeResolver = $typeResolver; + + return $this; + } + + public function group(): string|\BackedEnum + { + return Group::Resolve; + } + + /** + * The enums in a member's type, unless its schema references something or lists its own + * `enum`, which `Types` keeps in place of a reference. + */ + protected function collect(?\Reflector $reflector, ?OA\Schema $schema = null): void + { + if ($schema instanceof OA\Schema && ($schema->ref !== null || $schema->enum !== null)) { + return; + } + + if ($reflector instanceof \ReflectionProperty || $reflector instanceof \ReflectionParameter || $reflector instanceof \ReflectionMethod) { + $this->collectFromType($this->typeResolver->resolve($reflector)); + } + } + + protected function collectFromType(?SchemaType $type): void + { + if (!$type instanceof SchemaType) { + return; + } + + $class = $type->isRef() && is_string($type->type) ? ltrim($type->type, '\\') : null; + if ($class !== null && is_a($class, \UnitEnum::class, true)) { + $this->enums[$class] = true; + } + + $this->collectFromType($type->items); + $this->collectFromType($type->additionalProperties instanceof SchemaType ? $type->additionalProperties : null); + foreach ([...$type->oneOf ?? [], ...$type->allOf ?? [], ...$type->anyOf ?? []] as $member) { + $this->collectFromType($member); + } + } +} diff --git a/upload/api/vendor/zircote/swagger-php/src/Augmenter/Inheritance/Operations.php b/upload/api/vendor/zircote/swagger-php/src/Augmenter/Inheritance/Operations.php index ee51b8b8..0be6c367 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Augmenter/Inheritance/Operations.php +++ b/upload/api/vendor/zircote/swagger-php/src/Augmenter/Inheritance/Operations.php @@ -19,7 +19,8 @@ * composition can apply. * * Walks each PathItem class's ancestors: if an ancestor has no PathItem - * of its own, its operations are cloned and re-associated with the child class. The + * of its own, each child class gets a deep copy of its operations, re-associated with the child. + * A deep copy, so a change to one child's responses or parameters stays with that child. The * original operations (associated with the abstract parent) are removed since they * have no PathItem context of their own. * @@ -92,9 +93,9 @@ protected function cloneToChildren(Specification $specification, array $pathItem if (isset($ancestorOperations[$parentName])) { foreach ($ancestorOperations[$parentName] as $operation) { - $clone = clone $operation; - $clone->setReflector($reflector); - $specification->add($clone); + $copy = $operation->copy(); + $copy->setReflector($reflector); + $specification->add($copy); $toRemove[spl_object_id($operation)] = true; } } else { @@ -125,7 +126,7 @@ protected function discoverOperations(\ReflectionClass $ancestor, \ReflectionCla $operations = []; foreach ($this->attributeFactory->getDirectMethods($ancestor) as $method) { - foreach ($this->attributeFactory->fromReflector($method) as $root) { + foreach ($this->attributeFactory->fromMember($method) as $root) { if ($root instanceof OA\Operation) { $root->setReflector($childReflector); $operations[] = $root; diff --git a/upload/api/vendor/zircote/swagger-php/src/Augmenter/Inheritance/Schemas.php b/upload/api/vendor/zircote/swagger-php/src/Augmenter/Inheritance/Schemas.php index 27b80ea0..3dab4d89 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Augmenter/Inheritance/Schemas.php +++ b/upload/api/vendor/zircote/swagger-php/src/Augmenter/Inheritance/Schemas.php @@ -131,7 +131,9 @@ protected function expandInterfaces(OA\Schema $schema, \ReflectionClass $reflect protected function addAllOfRef(OA\Schema $schema, OA\Schema $referenced): void { $schema->allOf ??= []; - $name = $referenced->schema ?? $referenced->getShortClassName(); + // the explicit key or the class name, as `Names` will key it — not `of()`, whose + // title fallback would name a class schema after its title before `Names` has run + $name = $referenced->component ?? $referenced->getShortClassName(); if ($name !== null) { $schema->allOf[] = new OA\Schema(ref: JsonPointer::ref('components', 'schemas', $name)); } diff --git a/upload/api/vendor/zircote/swagger-php/src/Augmenter/Merge.php b/upload/api/vendor/zircote/swagger-php/src/Augmenter/Merge.php new file mode 100644 index 00000000..e36a48d2 --- /dev/null +++ b/upload/api/vendor/zircote/swagger-php/src/Augmenter/Merge.php @@ -0,0 +1,127 @@ + + */ +class Merge implements PipeInterface, LoggerAwareInterface +{ + use LoggerAwareTrait; + + /** + * @param TypedList $mergers + */ + public function __construct( + protected TypedList $mergers, + protected Group $group = Group::Reduce, + ) { + } + + public function __invoke(mixed $payload): mixed + { + if ($this->logger instanceof LoggerInterface) { + foreach ($this->mergers as $merger) { + if ($merger instanceof LoggerAwareInterface) { + $merger->setLogger($this->logger); + } + } + } + + // every root collection, discovered rather than listed: a new bucket on the + // `Specification` is reduced without this pass being told about it + foreach (get_object_vars($payload) as $property => $value) { + if (is_array($value)) { + $payload->{$property} = $this->reduce($value); + } + } + + return null; + } + + public function group(): string|\BackedEnum + { + return $this->group; + } + + /** + * @param array $attributes + * @return list + */ + protected function reduce(array $attributes): array + { + if (count($attributes) < 2) { + return array_values($attributes); + } + + $reduced = []; + $slots = []; + + foreach ($attributes as $attribute) { + $merger = $attribute instanceof AttributeInterface ? $this->mergerFor($attribute) : null; + $identity = $merger?->identity($attribute); + + if (!$merger instanceof MergerInterface || $identity === null) { + $reduced[] = $attribute; + continue; + } + + // two mergers never fold into each other: whoever claimed the type decides + $key = $merger::class . "\0" . $identity; + + if (!array_key_exists($key, $slots)) { + $slots[$key] = count($reduced); + $reduced[] = $attribute; + continue; + } + + $reduced[$slots[$key]] = $merger->merge($reduced[$slots[$key]], $attribute); + } + + // replacing a slot keeps the list in order; the analyser only loses track that it is one + return array_values($reduced); + } + + protected function mergerFor(AttributeInterface $attribute): ?MergerInterface + { + foreach ($this->mergers as $merger) { + if ($merger->supports($attribute::class)) { + return $merger; + } + } + + return null; + } +} diff --git a/upload/api/vendor/zircote/swagger-php/src/Augmenter/Names.php b/upload/api/vendor/zircote/swagger-php/src/Augmenter/Names.php index a4949082..bec48be6 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Augmenter/Names.php +++ b/upload/api/vendor/zircote/swagger-php/src/Augmenter/Names.php @@ -7,6 +7,7 @@ namespace OpenApi\Augmenter; use OpenApi\Contracts\AttributeInterface; +use OpenApi\Spec as OA; use OpenApi\Specification; use OpenApi\Specification\ComponentName; use OpenApi\Utils\PipeInterface; @@ -19,8 +20,11 @@ * to the *declaring* class, whose name is already taken by that class's own component, so * nothing is inferred and the component stays inline. * - * A parameter is the one exception: its `name` is its identity in OpenAPI, so a parameter - * component is keyed by it wherever it was declared, and falls back to the class name only + * A class carrying `#[OA\Components]` is the exception: it is a holder, so each of its members + * declares a component of its own, named after the member. + * + * A parameter is keyed differently: its `name` is its identity in OpenAPI, so a parameter + * component is keyed by it wherever it was declared, holder or not, and falls back to the class name only * when it has no name either. * * @implements PipeInterface @@ -29,9 +33,11 @@ class Names implements PipeInterface { /** * A security scheme is only ever declared inside `Components`, where the key is the whole - * point, so nothing is inferred for it. + * point, so nothing is inferred for it. A path item on a class is path-bound unless keyed + * by hand, and a media type is only ever a component by hand, so neither is named after + * the class either. */ - protected const SKIP_BUCKETS = ['securitySchemes']; + protected const SKIP_BUCKETS = ['securitySchemes', 'pathItems', 'mediaTypes']; public function __invoke(mixed $payload): mixed { @@ -52,7 +58,7 @@ public function group(): string|\BackedEnum protected function inferParameterNames(Specification $specification): void { foreach ($specification->parameters as $parameter) { - $parameter->parameter ??= $parameter->name; + $parameter->component ??= $parameter->name; } } @@ -62,14 +68,42 @@ protected function inferParameterNames(Specification $specification): void protected function inferClassNames(array $components): void { foreach ($components as $component) { - if (!$component->getReflector() instanceof \ReflectionClass) { + $name = $this->inferredName($component); + if ($name === null) { continue; } $field = ComponentName::keyField($component); if ($field !== null && $component->{$field} === null) { - $component->{$field} = $component->getShortClassName(); + $component->{$field} = $name; } } } + + /** + * The name a component without a key is filed under, or null to leave it unnamed. + * + * - declared on a class: the class's short name, so a `$ref` given as the class resolves to it + * - declared on a member of a class carrying `#[OA\Components]`: the member's own name, + * whatever kind of member it is + * - anywhere else: null. The declaring class's name belongs to that class's own component, + * so the component stays inline + * + * Only a missing key is filled, and a parameter has been keyed by its `name` before this runs. + */ + protected function inferredName(AttributeInterface $component): ?string + { + $reflector = $component->getReflector(); + + if ($reflector instanceof \ReflectionClass) { + return $component->getShortClassName(); + } + + $holder = $component->getClassReflector()?->getAttributes(OA\Components::class) ?? []; + if ($holder !== [] && ($reflector instanceof \ReflectionMethod || $reflector instanceof \ReflectionProperty || $reflector instanceof \ReflectionClassConstant || $reflector instanceof \ReflectionParameter)) { + return $reflector->getName(); + } + + return null; + } } diff --git a/upload/api/vendor/zircote/swagger-php/src/Augmenter/OperationIdTemplates.php b/upload/api/vendor/zircote/swagger-php/src/Augmenter/OperationIdTemplates.php new file mode 100644 index 00000000..ca351b70 --- /dev/null +++ b/upload/api/vendor/zircote/swagger-php/src/Augmenter/OperationIdTemplates.php @@ -0,0 +1,47 @@ + + */ +class OperationIdTemplates implements PipeInterface +{ + public function __invoke(mixed $payload): mixed + { + foreach ($payload->operations as $operation) { + if ($operation->operationId !== null) { + $operation->operationId = $this->expand($operation->operationId, $operation); + } + } + + return null; + } + + public function group(): string|\BackedEnum + { + return Group::Augment; + } + + protected function expand(string $operationId, OA\Operation $operation): string + { + $placeholders = array_filter([ + '{method}' => $operation->method, + ], static fn (?string $value): bool => $value !== null); + + return strtr($operationId, $placeholders); + } +} diff --git a/upload/api/vendor/zircote/swagger-php/src/Augmenter/OperationIds.php b/upload/api/vendor/zircote/swagger-php/src/Augmenter/OperationIds.php index 526b2cd3..8a6ad695 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Augmenter/OperationIds.php +++ b/upload/api/vendor/zircote/swagger-php/src/Augmenter/OperationIds.php @@ -14,6 +14,9 @@ /** * Generates operationId for operations that don't have one explicitly set. * + * The id is derived from method, path and the declaring method, function or class. An + * operation with no reflector is identified by method and path alone. + * * @implements PipeInterface */ class OperationIds implements PipeInterface @@ -57,19 +60,19 @@ protected function generateId(OA\Operation $operation): ?string $reflector = $operation->getReflector(); $source = match (true) { - $reflector instanceof \ReflectionMethod => $operation->getClassName() . '::' . $reflector->getName(), + $reflector instanceof \ReflectionMethod => $operation->getSourceLocation()->qualifiedMethod(), $reflector instanceof \ReflectionFunction => $reflector->getName(), $reflector instanceof \ReflectionClass => $reflector->getName(), default => null, }; - if ($source === null) { - return null; - } - $method = strtoupper($operation->method ?? 'GET'); $path = $operation->path ?? ''; + if ($source === null) { + return $path === '' ? null : $method . '::' . $path; + } + return $method . '::' . $path . '::' . $source; } } diff --git a/upload/api/vendor/zircote/swagger-php/src/Augmenter/OperationTags.php b/upload/api/vendor/zircote/swagger-php/src/Augmenter/OperationTags.php new file mode 100644 index 00000000..397d267c --- /dev/null +++ b/upload/api/vendor/zircote/swagger-php/src/Augmenter/OperationTags.php @@ -0,0 +1,73 @@ + + */ +class OperationTags implements PipeInterface +{ + public function __invoke(mixed $payload): mixed + { + $byMethod = []; + $byClass = []; + + foreach ($payload->tags as $tag) { + if ($tag->name === null) { + continue; + } + + $reflector = $tag->getReflector(); + if ($reflector instanceof \ReflectionMethod) { + $byMethod[$this->methodKey($reflector)][] = $tag->name; + } elseif ($reflector instanceof \ReflectionClass) { + $byClass[$reflector->getName()][] = $tag->name; + } + } + + if ($byMethod === [] && $byClass === []) { + return null; + } + + foreach ($payload->operations as $operation) { + $reflector = $operation->getReflector(); + $added = [ + ...($reflector instanceof \ReflectionMethod ? $byMethod[$this->methodKey($reflector)] ?? [] : []), + ...$byClass[$operation->getClassName() ?? ''] ?? [], + ]; + + if ($added !== []) { + $operation->tags = array_values(array_unique([...$operation->tags ?? [], ...$added])); + } + } + + return null; + } + + public function group(): string|\BackedEnum + { + return Group::Resolve; + } + + protected function methodKey(\ReflectionMethod $method): string + { + return $method->getDeclaringClass()->getName() . '::' . $method->getName(); + } +} diff --git a/upload/api/vendor/zircote/swagger-php/src/Augmenter/Parameters.php b/upload/api/vendor/zircote/swagger-php/src/Augmenter/Parameters.php new file mode 100644 index 00000000..126361bf --- /dev/null +++ b/upload/api/vendor/zircote/swagger-php/src/Augmenter/Parameters.php @@ -0,0 +1,65 @@ + + */ +class Parameters implements PipeInterface +{ + public function __invoke(mixed $payload): mixed + { + $index = null; + + foreach ([...$payload->operations, ...$payload->pathItems] as $holder) { + foreach ($holder->parameters ?? [] as $parameter) { + if ($parameter->ref === null || ($parameter->name !== null && $parameter->in !== null)) { + continue; + } + + $index ??= $payload->buildComponentIndex(); + $this->fillFromComponent($parameter, $index); + } + } + + return null; + } + + public function group(): string|\BackedEnum + { + return Group::Resolve; + } + + protected function fillFromComponent(OA\Parameter $parameter, ComponentIndex $index): void + { + $ref = $parameter->ref instanceof OA\Schema\Ref ? $parameter->ref->ref : $parameter->ref; + $target = is_string($ref) ? $index->findParameter($ref) : null; + if (!$target instanceof OA\Parameter) { + return; + } + + $parameter->name ??= $target->name; + $parameter->in ??= $target->in; + } +} diff --git a/upload/api/vendor/zircote/swagger-php/src/Augmenter/PathItems.php b/upload/api/vendor/zircote/swagger-php/src/Augmenter/PathItems.php index 7b3b06ff..8eaaf9ce 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Augmenter/PathItems.php +++ b/upload/api/vendor/zircote/swagger-php/src/Augmenter/PathItems.php @@ -12,14 +12,18 @@ use OpenApi\Utils\PipeInterface; /** - * Resolves PathItem prefixes, clones metadata to operations, and sets path-level output. + * Resolves PathItem prefixes, copies metadata to operations, and sets path-level output. * * Composes path prefixes from the PathItems governing each operation's class, prepends them - * to operation paths, clones tags/security/responses to operations that don't declare their + * to operation paths, copies tags/security/responses to operations that don't declare their * own, and marks PathItems that have spec-level output (parameters, summary, description, - * servers) with their resolved path. + * servers) with their resolved path. A PathItem governing several paths is written once per + * path. * - * The ancestor walk itself belongs to `Specification\PathItemHierarchy`. + * Security requirements, responses and the per-path PathItems are deep copies, so a change + * made through one operation or path stays there. + * + * The ancestor walk and the prefix composition belong to `Specification\PathItemHierarchy`. * * @implements PipeInterface */ @@ -39,11 +43,7 @@ public function __invoke(mixed $payload): mixed continue; } - $prefix = $this->resolvePrefix($chain); - if ($prefix !== '' && $operation->path !== null) { - $operationPath = ltrim($operation->path, '/'); - $operation->path = $operationPath !== '' ? $prefix . '/' . $operationPath : $prefix; - } + $operation->path = $hierarchy->pathFor($operation); $this->cloneMetadata($chain, $operation); } @@ -58,23 +58,6 @@ public function group(): string|\BackedEnum return Group::Resolve; } - /** - * Compose the path prefix declared across a chain, outermost first. - * - * @param list $chain - */ - protected function resolvePrefix(array $chain): string - { - $parts = []; - foreach ($chain as $pathItem) { - if ($pathItem->prefix !== null && ($part = trim($pathItem->prefix, '/')) !== '') { - $parts[] = $part; - } - } - - return $parts !== [] ? '/' . implode('/', $parts) : ''; - } - /** * @param list $chain */ @@ -94,7 +77,7 @@ protected function cloneMetadata(array $chain, OA\Operation $operation): void foreach ($merged['security'] as $req) { if (!isset($existingSchemes[$req->scheme])) { $operation->security ??= []; - $operation->security[] = $req; + $operation->security[] = $req->copy(); } } } @@ -110,7 +93,7 @@ protected function cloneMetadata(array $chain, OA\Operation $operation): void foreach ($merged['responses'] as $response) { if ($response->response !== null && !isset($existingCodes[(string) $response->response])) { $operation->responses ??= []; - $operation->responses[] = $response; + $operation->responses[] = $response->copy(); } } } @@ -153,6 +136,10 @@ protected function resolvePathItemPaths(Specification $specification, PathItemHi $pathsByPathItem = $this->collectOperationPaths($specification, $hierarchy); foreach ($specification->pathItems as $pathItem) { + if ($pathItem->component !== null) { + continue; + } + $this->mergeAncestorParameters($pathItem, $hierarchy); if (!$this->hasSpecProperties($pathItem)) { @@ -169,9 +156,9 @@ protected function resolvePathItemPaths(Specification $specification, PathItemHi if ($pathItem->path === null) { $pathItem->path = $path; } elseif ($pathItem->path !== $path) { - $clone = clone $pathItem; - $clone->path = $path; - $specification->pathItems[] = $clone; + $copy = $pathItem->copy(); + $copy->path = $path; + $specification->pathItems[] = $copy; } } } diff --git a/upload/api/vendor/zircote/swagger-php/src/Augmenter/Properties.php b/upload/api/vendor/zircote/swagger-php/src/Augmenter/Properties.php new file mode 100644 index 00000000..10808532 --- /dev/null +++ b/upload/api/vendor/zircote/swagger-php/src/Augmenter/Properties.php @@ -0,0 +1,172 @@ + + */ +class Properties implements PipeInterface +{ + public function __invoke(mixed $payload): mixed + { + $payload->getWalker()->eachSchema(function (OA\Schema $schema): void { + if (!$schema->properties || $schema->getMeta(HybridBridge::class) === true) { + return; + } + + $required = []; + foreach ($schema->properties as $property) { + $reflector = $property->getReflector(); + if (!$property instanceof OA\Property + || $property->property === null + || (!$reflector instanceof \ReflectionProperty && !$reflector instanceof \ReflectionParameter) + ) { + continue; + } + + $default = $this->defaultOf($reflector); + + if ($default === Undefined::UNDEFINED && $reflector->getType() !== null && !$reflector->getType()->allowsNull()) { + $required[] = $property->property; + } + + $this->applyDefault($property, $default); + } + + if ($schema->required === null && $required !== []) { + $schema->required = $required; + } + }); + + return null; + } + + public function group(): string|\BackedEnum + { + return Group::Resolve; + } + + /** + * The member's PHP default, or `Undefined::UNDEFINED` where it has none. + */ + protected function defaultOf(\ReflectionProperty|\ReflectionParameter $reflector): mixed + { + if ($reflector instanceof \ReflectionProperty && $reflector->isPromoted()) { + foreach ($reflector->getDeclaringClass()->getConstructor()?->getParameters() ?? [] as $parameter) { + if ($parameter->getName() === $reflector->getName()) { + $reflector = $parameter; + break; + } + } + } + + if ($reflector instanceof \ReflectionParameter) { + return $reflector->isDefaultValueAvailable() ? $reflector->getDefaultValue() : Undefined::UNDEFINED; + } + + // an untyped property with no default reports `null`, which is not taken anyway + return $reflector->hasDefaultValue() ? $reflector->getDefaultValue() : Undefined::UNDEFINED; + } + + protected function applyDefault(OA\Property $property, mixed $default): void + { + $value = $default === Undefined::UNDEFINED ? null : $this->documentValue($default); + if ($value === null) { + return; + } + + $schema = $property->schema ?? new OA\Schema(); + if (!Undefined::isDefault($schema->default) || !$this->admits($schema, $value, $default instanceof \UnitEnum)) { + return; + } + + $types = (array) $schema->type; + if ($value === [] && in_array('object', $types, true) && !in_array('array', $types, true)) { + $value = new \stdClass(); + } + + $schema->default = $value; + $property->schema = $schema; + } + + /** + * The value as the document holds it, or `null` where it cannot hold it. + */ + protected function documentValue(mixed $value): mixed + { + if ($value instanceof \UnitEnum) { + return TypeMapper::caseValue($value); + } + + if (is_scalar($value)) { + return $value; + } + + if (!is_array($value)) { + return null; + } + + $values = []; + foreach ($value as $key => $item) { + $values[$key] = $this->documentValue($item); + if ($values[$key] === null) { + return null; + } + } + + return $values; + } + + protected function admits(OA\Schema $schema, mixed $value, bool $fromEnum): bool + { + // a reference can only be checked where the value came from the enum it points to + if ($schema->ref !== null) { + return $fromEnum; + } + + $types = (array) $schema->type; + if ($types === []) { + return true; + } + + $valueType = TypeMapper::valueType($value); + foreach ($types as $type) { + // an integer is a number too, and an empty array is an empty object as well as an empty list + if ($type === $valueType || ($type === 'number' && $valueType === 'integer') || ($type === 'object' && $value === [])) { + return true; + } + } + + return false; + } +} diff --git a/upload/api/vendor/zircote/swagger-php/src/Augmenter/Refs.php b/upload/api/vendor/zircote/swagger-php/src/Augmenter/Refs.php index 715c8a28..9c3972e3 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Augmenter/Refs.php +++ b/upload/api/vendor/zircote/swagger-php/src/Augmenter/Refs.php @@ -9,6 +9,8 @@ use OpenApi\Contracts\AttributeInterface; use OpenApi\Spec as OA; use OpenApi\Specification; +use OpenApi\Specification\ComponentIndex; +use OpenApi\Specification\ComponentName; use OpenApi\Utils\JsonPointer; use OpenApi\Utils\PipeInterface; use Psr\Log\LoggerAwareInterface; @@ -19,6 +21,7 @@ * * Builds a map of class names to their component paths and rewrites * any $ref that looks like a FQCN into the proper #/components/... path. + * A ref that is neither a known class nor a pointer to an existing component is reported. * * @implements PipeInterface */ @@ -38,10 +41,8 @@ public function __invoke(mixed $payload): mixed $index = $payload->buildComponentIndex(); $refMap = $index->buildRefMap(); - if ($refMap !== []) { - $this->resolveRefRefs($payload); - $this->resolveFQCNRefs($payload, $refMap); - } + $this->resolveRefRefs($payload); + $this->resolveFQCNRefs($payload, $refMap, $index); // deduplicate once every ref carries its final value: a FQCN and the component // pointer added by Augmenter\Inheritance name the same schema but differ as strings @@ -79,16 +80,21 @@ protected function resolveRefRefs(Specification $specification): void /** * @param array $refMap */ - protected function resolveFQCNRefs(Specification $specification, array $refMap): void + protected function resolveFQCNRefs(Specification $specification, array $refMap, ComponentIndex $index): void { $unresolved = []; + $dangling = []; - $specification->getWalker()->eachRef(function (AttributeInterface $attribute) use ($refMap, &$unresolved): void { - if (!property_exists($attribute, 'ref') || $attribute->ref === null || str_starts_with($attribute->ref, '#/')) { + $specification->getWalker()->eachRef(function (AttributeInterface $attribute) use ($refMap, $index, &$unresolved, &$dangling): void { + if (!property_exists($attribute, 'ref') || !is_string($attribute->ref)) { return; } - if (isset($refMap[$attribute->ref])) { + if (str_starts_with($attribute->ref, '#/')) { + if ($this->isDangling($attribute->ref, $index)) { + $dangling[$attribute->ref] = true; + } + } elseif (isset($refMap[$attribute->ref])) { $attribute->ref = $refMap[$attribute->ref]; } else { $unresolved[$attribute->ref] = true; @@ -96,8 +102,45 @@ protected function resolveFQCNRefs(Specification $specification, array $refMap): }); foreach (array_keys($unresolved) as $ref) { - $this->logger?->warning("Ref: unresolved reference '{$ref}' — no matching component found"); + $pointer = $this->pointerFor((string) $ref, $index); + $this->logger?->warning($pointer !== null + ? "Ref: unresolved reference '{$ref}' — a component of that name exists, use '{$pointer}'" + : "Ref: unresolved reference '{$ref}' — no matching component found"); + } + + foreach (array_keys($dangling) as $ref) { + $this->logger?->warning("Ref: dangling reference '{$ref}' — no such component"); + } + } + + /** + * A pointer into a component bucket whose component does not exist. Pointers anywhere else are not checked, + * and neither are security schemes: the walker makes a pointer for each scheme a requirement names. + */ + protected function isDangling(string $ref, ComponentIndex $index): bool + { + $segments = explode('/', $ref); + if (count($segments) < 4 || $segments[1] !== 'components' || $segments[2] === 'securitySchemes' + || !in_array($segments[2], ComponentName::BUCKETS, true)) { + return false; + } + + return !$index->find(JsonPointer::ref('components', $segments[2], JsonPointer::decode($segments[3]))) instanceof AttributeInterface; + } + + /** + * The pointer to the component a bare name matches, if there is one. + */ + protected function pointerFor(string $name, ComponentIndex $index): ?string + { + foreach (ComponentName::BUCKETS as $bucket) { + $pointer = JsonPointer::ref('components', $bucket, $name); + if ($index->find($pointer) instanceof AttributeInterface) { + return $pointer; + } } + + return null; } /** @@ -105,9 +148,9 @@ protected function resolveFQCNRefs(Specification $specification, array $refMap): */ protected function resolveDiscriminatorMappings(Specification $specification, array $refMap): void { - foreach ($specification->schemas as $schema) { + $specification->getWalker()->eachSchema(function (OA\Schema $schema) use ($refMap): void { if (!$schema->discriminator instanceof OA\Discriminator || $schema->discriminator->mapping === null) { - continue; + return; } foreach ($schema->discriminator->mapping as $value => $type) { @@ -118,7 +161,7 @@ protected function resolveDiscriminatorMappings(Specification $specification, ar $schema->discriminator->mapping[$value] = $refMap[$type]; } } - } + }); } /** @@ -132,8 +175,9 @@ protected function resolveAllOfPropertyRefs(Specification $specification): void if ($schema->allOf !== null && $schema->properties === null) { foreach ($schema->allOf as $index => $allOf) { if ($allOf instanceof OA\Schema && $allOf->properties !== null) { - if ($schema->schema !== null) { - $candidates[$schema->schema] = $index; + $name = ComponentName::of($schema); + if ($name !== null) { + $candidates[$name] = $index; } } } @@ -168,7 +212,7 @@ protected function resolveAllOfPropertyRefs(Specification $specification): void */ protected function mergeAllOf(OA\Schema $schema): void { - if ($schema->allOf === null || $schema->properties === null) { + if ($schema->allOf === null || $schema->properties === null || $schema->properties === []) { return; } @@ -184,8 +228,7 @@ protected function dedupAllOfRefs(OA\Schema $schema): void $unique = []; foreach ($schema->allOf as $ii => $allOf) { - // resolveRefRefs() has collapsed every Schema\Ref into its string by now, but only - // runs when the ref map is non-empty; key on the string form defensively + // resolveRefRefs() has collapsed every Schema\Ref into its string by now $ref = $allOf->ref instanceof OA\Schema\Ref ? $allOf->ref->ref : $allOf->ref; if (is_string($ref)) { if (isset($unique[$ref])) { diff --git a/upload/api/vendor/zircote/swagger-php/src/Augmenter/SecurityRequirements.php b/upload/api/vendor/zircote/swagger-php/src/Augmenter/SecurityRequirements.php new file mode 100644 index 00000000..fd223997 --- /dev/null +++ b/upload/api/vendor/zircote/swagger-php/src/Augmenter/SecurityRequirements.php @@ -0,0 +1,72 @@ + + */ +class SecurityRequirements implements PipeInterface, LoggerAwareInterface +{ + use LoggerAwareTrait; + + public function __invoke(mixed $payload): mixed + { + $this->checkHolder($payload->openapi); + foreach ($payload->pathItems as $pathItem) { + $this->checkHolder($pathItem); + } + foreach ($payload->operations as $operation) { + $this->checkHolder($operation); + } + + return null; + } + + public function group(): string|\BackedEnum + { + return Group::Resolve; + } + + protected function checkHolder(OA\OpenApi|OA\PathItem|OA\Operation $holder): void + { + if ($holder->security === null || $holder->security === []) { + return; + } + + $security = []; + foreach ($holder->security as $item) { + if (is_string($item)) { + $this->logger?->warning(sprintf( + 'Security requirement "%s" is a bare scheme name and was dropped, use new Security\Requirement(scheme: \'%s\') in %s', + $item, + $item, + $holder->getSourceLocation(), + )); + continue; + } + + $security[] = $item; + } + + $holder->security = $security !== [] ? $security : null; + } +} diff --git a/upload/api/vendor/zircote/swagger-php/src/Augmenter/Shortcuts.php b/upload/api/vendor/zircote/swagger-php/src/Augmenter/Shortcuts.php index 7193ee59..dee68589 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Augmenter/Shortcuts.php +++ b/upload/api/vendor/zircote/swagger-php/src/Augmenter/Shortcuts.php @@ -13,18 +13,23 @@ use OpenApi\Utils\PipeInterface; /** - * Resolves shortcut attributes. + * Resolves shortcut attributes into the plain attributes they stand for. + * + * A shortcut carries schema keywords on an attribute that holds a schema, so the common case + * needs no nested `OA\Schema`. This augmenter moves those keywords to where the document has + * them. * * Handles: - * * `OA\MediaType\Json` - * * `OA\MediaType\Xml` - * * `OA\Schema\Items` + * * `OA\MediaType\Json` and `OA\MediaType\Xml`: their schema keywords move into the media + * type's `schema`; + * * `OA\Schema\Items`: an array schema, whose item keywords move into a new `items` schema. * * @implements PipeInterface */ class Shortcuts implements PipeInterface { - private const ITEMS_KEEP_PROPERTIES = ['schema', 'title', 'description', 'deprecated', 'readOnly', 'writeOnly', 'xml', 'externalDocs', 'x', 'attachables']; + /** What stays on the array schema an `OA\Schema\Items` becomes, besides the array keywords. */ + private const ITEMS_KEEP_PROPERTIES = ['schema', 'component', 'title', 'description', 'deprecated', 'readOnly', 'writeOnly', 'xml', 'externalDocs', 'x', 'attachables']; public function __invoke(mixed $payload): mixed { @@ -39,6 +44,9 @@ public function group(): string|\BackedEnum return Group::Resolve; } + /** + * Every `Json` or `Xml` media type in a `content` list, wherever it is nested. + */ protected function processMediaTypes(Specification $specification): void { $specification->getWalker()->visit(AttributeInterface::class, function (AttributeInterface $attribute): void { @@ -52,6 +60,11 @@ protected function processMediaTypes(Specification $specification): void }); } + /** + * Move the media type's schema keywords (`ref`, `type`, `items`, `properties`, `required`) + * into its `schema`, creating one when there is none. With a `schema` already set, a keyword + * moves only where the schema has no value of its own. + */ protected function processMediaType(OA\MediaType\Json|OA\MediaType\Xml $mediaType): void { // spelled out per property rather than looped over a list of property names: the @@ -100,27 +113,57 @@ protected function processMediaType(OA\MediaType\Json|OA\MediaType\Xml $mediaTyp } } + /** + * Every `OA\Schema\Items` in the specification, wherever it is nested, except one held directly + * in the `items` of a schema that is not an `Items` itself. + * + * There it is the item schema, not an array of them: `new OA\Schema(type: 'array', items: new + * OA\Schema\Items(type: 'string'))` is an array of strings. In the `items` of another `Items` + * it is an array, so an array of arrays. + */ protected function processSchemaItems(Specification $specification): void { - $specification->getWalker()->visit(AttributeInterface::class, function (AttributeInterface $attribute): void { - if (property_exists($attribute, 'schema') && $attribute->schema instanceof OA\Schema\Items) { - $this->processSchemaItem($attribute->schema); + $walker = $specification->getWalker(); + + $itemSchemas = new \SplObjectStorage(); + $walker->eachSchema(static function (OA\Schema $schema) use ($itemSchemas): void { + if (!$schema instanceof OA\Schema\Items && $schema->items instanceof OA\Schema\Items) { + $itemSchemas->offsetSet($schema->items); + } + }); + + // the walker visits children before their parent, so a nested `Items` is resolved first + $walker->visit(OA\Schema\Items::class, function (OA\Schema\Items $items) use ($itemSchemas): void { + if (!$itemSchemas->offsetExists($items)) { + $this->processSchemaItem($items); } }); } + /** + * Turn an `OA\Schema\Items` into the array schema it stands for, in place. + * + * `#[OA\Schema\Items(ref: Tag::class, minItems: 1)]` describes an array of `Tag`. The array + * keywords and the metadata in `ITEMS_KEEP_PROPERTIES` stay on the attribute, which becomes + * `type: array`. Every other `OA\Schema` keyword it sets moves into a new `items` schema. + * With nothing to move and no `items`, the items schema is empty: an array of anything. It + * does not recurse: an `Items` in `items` is resolved by the caller, before this one. + */ protected function processSchemaItem(OA\Schema\Items $items): void { - if ($items->items instanceof OA\Schema\Items) { - $this->processSchemaItem($items->items); - } - $itemKeep = [...OA\Schema::ARRAY_PROPERTIES, ...self::ITEMS_KEEP_PROPERTIES]; + // the keywords to move are read off `OA\Schema`'s constructor, so a keyword added there + // moves without being listed here. Each one becomes an argument for the new items schema + // and is cleared on the array. $innerArgs = []; - foreach ((new \ReflectionClass(OA\Schema::class))->getConstructor()->getParameters() as $param) { + $schema = new \ReflectionClass(OA\Schema::class); + foreach ($schema->getConstructor()->getParameters() as $param) { $prop = $param->getName(); - if (in_array($prop, $itemKeep, true)) { + // public properties only: the constructor also takes arguments that are not schema + // keywords and have no public property of the same name. `fan` is one: it says where + // the attribute nests, belongs to the array, and is not a keyword the items would carry. + if (in_array($prop, $itemKeep, true) || !$schema->hasProperty($prop) || !$schema->getProperty($prop)->isPublic()) { continue; } $value = $items->{$prop}; diff --git a/upload/api/vendor/zircote/swagger-php/src/Augmenter/TypeAliases.php b/upload/api/vendor/zircote/swagger-php/src/Augmenter/TypeAliases.php new file mode 100644 index 00000000..2c0a7898 --- /dev/null +++ b/upload/api/vendor/zircote/swagger-php/src/Augmenter/TypeAliases.php @@ -0,0 +1,160 @@ + + */ +class TypeAliases implements PipeInterface, LoggerAwareInterface +{ + use LoggerAwareTrait; + + protected const UNMAPPED = ['mixed', 'byte']; + + /** + * A schema nested in a `schema:` argument has no reflector of its own, so a report about it + * names the attribute holding it. + * + * @var \SplObjectStorage + */ + protected \SplObjectStorage $holders; + + public function __construct( + protected bool $enabled = true, + ) { + } + + public function __invoke(mixed $payload): mixed + { + if (!$this->enabled) { + return null; + } + + $this->holders = new \SplObjectStorage(); + $payload->getWalker()->visit(AttributeInterface::class, function (AttributeInterface $attribute): void { + if (property_exists($attribute, 'schema') && $attribute->schema instanceof OA\Schema) { + $this->holders[$attribute->schema] = $attribute; + } + }); + + $payload->getWalker()->eachSchema(function (OA\Schema $schema): void { + if (is_string($schema->type)) { + $this->translate($schema); + } elseif (is_array($schema->type)) { + $this->translateList($schema); + } + }); + + return null; + } + + public function setEnabled(bool $enabled): static + { + $this->enabled = $enabled; + + return $this; + } + + public function group(): string|\BackedEnum + { + return Group::Resolve; + } + + protected function translate(OA\Schema $schema): void + { + /** @var string $type */ + $type = $schema->type; + $mapped = $this->lookup($type); + if ($mapped === null) { + return; + } + + [$target, $format] = is_array($mapped) ? $mapped : [$mapped, null]; + if ($target === $type) { + return; + } + + $schema->type = $target; + if ($format !== null) { + $schema->format ??= $format; + } + + $this->report($schema, $type, $format === null ? "\"$target\"" : "\"$target\" with format \"$format\""); + } + + protected function translateList(OA\Schema $schema): void + { + /** @var list $types */ + $types = $schema->type; + + foreach ($types as $index => $type) { + $mapped = $this->lookup($type); + if ($mapped === null) { + continue; + } + + $target = is_array($mapped) ? $mapped[0] : $mapped; + if ($target === $type) { + continue; + } + + $types[$index] = $target; + $this->report($schema, $type, "\"$target\""); + } + + $schema->type = $types; + } + + /** + * @return string|array{string, string}|null + */ + protected function lookup(string $type): string|array|null + { + $key = strtolower($type); + + if (in_array($key, static::UNMAPPED, true) || !array_key_exists($key, TypeMapper::NATIVE_TYPE_MAP)) { + return null; + } + + return TypeMapper::NATIVE_TYPE_MAP[$key]; + } + + protected function report(OA\Schema $schema, string $alias, string $replacement): void + { + $located = !$schema->getReflector() instanceof \Reflector && isset($this->holders[$schema]) ? $this->holders[$schema] : $schema; + + $this->logger?->warning(sprintf( + 'Schema type "%s" is a classic alias, use %s (translation removed in 8.0; rector rule %s rewrites it) in %s', + $alias, + $replacement, + TypeAliasesRector::class, + $located->getSourceLocation(), + )); + } +} diff --git a/upload/api/vendor/zircote/swagger-php/src/Augmenter/Types.php b/upload/api/vendor/zircote/swagger-php/src/Augmenter/Types.php index cc590a35..0270e8c3 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Augmenter/Types.php +++ b/upload/api/vendor/zircote/swagger-php/src/Augmenter/Types.php @@ -6,12 +6,16 @@ namespace OpenApi\Augmenter; +use OpenApi\HybridBridge; use OpenApi\Spec as OA; use OpenApi\Specification; use OpenApi\Type\SchemaType; +use OpenApi\Type\TypeMapper; use OpenApi\Type\TypeResolver; use OpenApi\Undefined; use OpenApi\Utils\PipeInterface; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerInterface; /** * Infers schema type, format, nullable, items, etc. from PHP type declarations and docblocks. @@ -21,8 +25,19 @@ * * @implements PipeInterface */ -class Types implements PipeInterface +class Types implements PipeInterface, LoggerAwareInterface { + protected ?LoggerInterface $logger = null; + + /** @var array class name => component pointer, for the components that exist */ + protected array $refMap = []; + + /** the member whose type is being applied, for the warning when a class in it has no schema */ + protected ?\Reflector $subject = null; + + /** @var \SplObjectStorage schemas whose `type` this augmenter inferred, not the author */ + protected \SplObjectStorage $inferredTypes; + public function __construct( protected TypeResolver $typeResolver = new TypeResolver(), ) { @@ -30,6 +45,9 @@ public function __construct( public function __invoke(mixed $payload): mixed { + $this->refMap = $payload->buildComponentIndex()->buildRefMap(); + $this->inferredTypes = new \SplObjectStorage(); + $payload->getWalker()->eachSchema(function (OA\Schema $schema): void { $this->inferSchemaType($schema); $this->walkSchema($schema); @@ -65,10 +83,22 @@ public function __invoke(mixed $payload): mixed public function setTypeResolver(TypeResolver $typeResolver): static { $this->typeResolver = $typeResolver; + if ($this->logger instanceof LoggerInterface) { + $this->typeResolver->setLogger($this->logger); + } return $this; } + /** + * Passes the logger to the type resolver, including one set later. + */ + public function setLogger(LoggerInterface $logger): void + { + $this->logger = $logger; + $this->typeResolver->setLogger($logger); + } + public function group(): string|\BackedEnum { return Group::Resolve; @@ -82,8 +112,10 @@ protected function inferSchemaType(OA\Schema $schema): void if ($schema->items instanceof OA\Schema) { $schema->type = 'array'; - } elseif ($schema->properties || $schema->allOf || $schema->patternProperties) { + $this->inferredTypes->offsetSet($schema); + } elseif ($schema->properties || $schema->patternProperties) { $schema->type = 'object'; + $this->inferredTypes->offsetSet($schema); } } @@ -143,7 +175,7 @@ protected function augmentRequestBody(OA\RequestBody $requestBody): void $resolved = $this->typeResolver->resolve($reflector); if ($resolved instanceof SchemaType) { - if ($resolved->type !== null && $resolved->isRef()) { + if ($resolved->type !== null && $resolved->isRef() && $this->isComponent($resolved->type)) { $requestBody->ref = $resolved->type; } @@ -201,6 +233,8 @@ protected function augmentProperty(OA\Property $property): void return; } + $this->subject = $reflector; + if (!$property->schema instanceof OA\Schema) { $property->schema = $this->schemaTypeToSchema($resolved); } else { @@ -217,13 +251,8 @@ protected function augmentConstantProperty(OA\Property $property, \ReflectionCla if (Undefined::isDefault($schema->const)) { $schema->const = $value; } - $schema->type ??= match (true) { - is_string($value) => 'string', - is_int($value) => 'integer', - is_float($value) => 'number', - is_bool($value) => 'boolean', - default => null, - }; + // an array constant stays untyped, as it always has + $schema->type ??= is_array($value) ? null : TypeMapper::valueType($value); $property->schema = $schema; } @@ -238,6 +267,8 @@ protected function augmentMethodProperty(OA\Property $property, \ReflectionMetho return; } + $this->subject = $method; + if (!$property->schema instanceof OA\Schema) { $property->schema = $this->schemaTypeToSchema($resolved); } else { @@ -283,13 +314,26 @@ protected function augmentParameter(OA\Parameter $parameter): void $resolved->nullable = null; } + $this->subject = $reflector; + if (!$parameter->schema instanceof OA\Schema) { $parameter->schema = $this->schemaTypeToSchema($resolved); } else { $this->mergeIntoSchema($parameter->schema, $resolved); } - $parameter->required ??= !$isNullable; + // a default means a value exists when the parameter is left out. A path parameter is required + // whatever its default, and one converted from classic keeps classic's rule + $hasDefault = $reflector->isDefaultValueAvailable() + && $parameter->in !== 'path' + && $parameter->getMeta(HybridBridge::class) !== true; + + $parameter->required ??= !$isNullable && !$hasDefault; + } + + protected function isComponent(string $class): bool + { + return isset($this->refMap[ltrim($class, '\\')]); } protected function schemaTypeToSchema(SchemaType $schemaType): OA\Schema @@ -306,7 +350,27 @@ protected function mergeIntoSchema(OA\Schema $schema, SchemaType $schemaType): v $this->applySchemaType($schema, $schemaType); } - $schema->nullable ??= $schemaType->nullable; + // a type the author wrote out is the whole type, so the PHP type's `null` is not added to it + if (!$this->hasExplicitType($schema)) { + $schema->nullable ??= $schemaType->nullable; + } + } + + /** + * Whether the author gave the schema its type, rather than a shortcut or this augmenter. + * + * `Schema\Items` is always an array, whatever its arguments say, and `inferSchemaType()` types + * a schema from its `items` or `properties`. + */ + protected function hasExplicitType(OA\Schema $schema): bool + { + if ($schema->oneOf !== null || $schema->allOf !== null || $schema->anyOf !== null) { + return true; + } + + return $schema->type !== null + && !$schema instanceof OA\Schema\Items + && !$this->inferredTypes->offsetExists($schema); } protected function applySchemaType(OA\Schema $schema, SchemaType $schemaType): void @@ -317,7 +381,15 @@ protected function applySchemaType(OA\Schema $schema, SchemaType $schemaType): v if ($schemaType->type !== null) { if ($schemaType->isRef()) { - $schema->ref = $schemaType->type; + // a class is referenced only when it is a component, and never in place of an explicit enum + if ($schema->enum === null) { + if ($this->isComponent($schemaType->type)) { + $schema->ref = $schemaType->type; + } else { + $subject = $this->subject instanceof \Reflector ? $this->typeResolver->describe($this->subject) : 'a schema'; + $this->logger?->warning("No schema for {$schemaType->type} in {$subject}, so its type is left out"); + } + } } else { $schema->type = $schemaType->type; } @@ -345,7 +417,15 @@ protected function applySchemaType(OA\Schema $schema, SchemaType $schemaType): v } if ($schemaType->oneOf !== null) { - $schema->oneOf = array_map($this->schemaTypeToSchema(...), $schemaType->oneOf); + $members = array_map($this->schemaTypeToSchema(...), $schemaType->oneOf); + // a member typed with a class that is no component matches anything, and so does the union + $unknown = array_filter( + $schemaType->oneOf, + fn (SchemaType $member): bool => $member->isRef() && is_string($member->type) && !$this->isComponent($member->type), + ); + if ($unknown === []) { + $schema->oneOf = $members; + } } if ($schemaType->allOf !== null) { diff --git a/upload/api/vendor/zircote/swagger-php/src/Builder.php b/upload/api/vendor/zircote/swagger-php/src/Builder.php index 47657fb4..0d06ea1f 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Builder.php +++ b/upload/api/vendor/zircote/swagger-php/src/Builder.php @@ -9,6 +9,7 @@ use OpenApi\Builder\Mode; use OpenApi\Builder\Result; use OpenApi\Contracts\CompilerInterface; +use OpenApi\Contracts\MergerInterface; use OpenApi\Loggers\CollectingLogger; use OpenApi\Utils\AttributeFactory; use OpenApi\Utils\ClassReflector; @@ -58,6 +59,11 @@ class Builder */ protected ?Utils\Pipeline $augmenters = null; + /** + * @var Utils\TypedList|null + */ + protected ?Utils\TypedList $mergers = null; + /** * @var callable|null */ @@ -65,6 +71,9 @@ class Builder protected ?AttributeFactory $attributeFactory = null; + /** @var list */ + protected array $specificationHooks = []; + /** * @param BuilderSource|iterable $source */ @@ -123,6 +132,9 @@ public function getResolver(): Resolver /** * Configure the resolver via callable. * + * Runs when called, against the resolver the builder holds; repeated calls configure the + * same instance. + * * @param callable(Resolver): (Resolver|void) $hook */ public function withResolver(callable $hook): static @@ -150,6 +162,9 @@ public function getAugmenters(): Utils\Pipeline /** * Configure the augmenter pipeline via callable. * + * Runs when called, against the pipeline the builder holds; repeated calls configure the + * same pipeline. + * * @param callable(Utils\Pipeline): (Utils\Pipeline|void) $hook */ public function withAugmenters(callable $hook): static @@ -159,6 +174,35 @@ public function withAugmenters(callable $hook): static return $this; } + /** + * @return Utils\TypedList + */ + public function getMergers(): Utils\TypedList + { + $this->mergers ??= new Utils\TypedList($this->getDefaultMergers()); + + return $this->mergers; + } + + /** + * Configure the merger chain via callable. + * + * Mergers decide what makes two attributes the same one and what the survivor is. They are + * tried in registration order and the first to claim a type handles it, so a merger for one + * type goes ahead of `Merge\LastWins`, which claims everything and ships last. + * + * Runs when called, against the list the builder holds; repeated calls configure the same + * list. + * + * @param callable(Utils\TypedList): (Utils\TypedList|void) $hook + */ + public function withMergers(callable $hook): static + { + $hook($this->getMergers()); + + return $this; + } + public function getAttributeFactory(): AttributeFactory { $this->attributeFactory ??= new AttributeFactory(); @@ -167,6 +211,11 @@ public function getAttributeFactory(): AttributeFactory } /** + * Configure the attribute factory via callable. Translators are registered here. + * + * Runs when called, against the factory the builder holds; repeated calls configure the + * same instance. + * * @param callable(AttributeFactory): (AttributeFactory|void) $hook */ public function withAttributeFactory(callable $hook): static @@ -176,12 +225,35 @@ public function withAttributeFactory(callable $hook): static return $this; } + /** + * Contribute to the `Specification` after assembly and before resolution. + * + * The callable receives the assembled `Specification` and adds attributes to it with + * `Specification::add()`. A `$ref` in a contribution resolves as one in a scanned attribute + * does, and every augmenter sees what was contributed. A contribution carrying a reflector + * is treated as the assembler's own; one without is metadata nothing else reaches. + * + * Runs during the build, once the `Specification` exists. Hooks accumulate: every one + * runs, in registration order. Classic mode assembles no `Specification`, so the hooks + * are never called there. + * + * @param callable(Specification): (Specification|void) $hook + */ + public function withSpecification(callable $hook): static + { + $this->specificationHooks[] = $hook; + + return $this; + } + /** * Hook to configure the underlying Generator. * * The callable receives a default Generator and may either modify it in-place * or return a fully configured instance. * + * Runs during the build, once the Generator exists. A second call replaces the first. + * * @param callable(Generator): (Generator|void) $hook */ public function withGenerator(callable $hook): static @@ -191,6 +263,12 @@ public function withGenerator(callable $hook): static return $this; } + /** + * Build the document. + * + * A `Builder` builds one document. To build another, configure a new `Builder`: a second + * call is not prevented, but what it reuses from the first is not part of the contract. + */ public function build(): Result { return match ($this->mode) { @@ -229,7 +307,10 @@ protected function doBuildClassic(): Result protected function doBuildSpec(bool $hybrid = false): Result { + $collecting = new CollectingLogger($this->getLogger()); + $attributeFactory = $this->getAttributeFactory(); + $attributeFactory->setLogger($collecting); $assembler = new Assembler(attributeFactory: $attributeFactory); $sourceScanner = new SourceScanner($this->getLogger()); @@ -255,21 +336,42 @@ protected function doBuildSpec(bool $hybrid = false): Result foreach ($sourceScanner->getReflectors() as $reflector) { if ($reflector instanceof \ReflectionClass) { $this->collectSafely($assembler, $reflector); + } else { + $this->getLogger()->warning(sprintf( + 'Skipping %s source %s: only ReflectionClass sources are read', + $reflector::class, + match (true) { + $reflector instanceof \ReflectionMethod, $reflector instanceof \ReflectionProperty => $reflector->class . '::' . $reflector->getName(), + method_exists($reflector, 'getName') => $reflector->getName(), + default => '', + }, + )); } } $specification = $assembler->getSpecification(); + foreach ($this->specificationHooks as $hook) { + $hook($specification); + } + + // every producer has run and nothing has read a component key yet; the resolver's + // index is the first reader + Specification\ComponentName::normalise($specification, deprecate: true); + + $this->getResolver()->setLogger($this->getLogger()); $this->getResolver()->resolve($assembler); if ($hybrid) { $this->doHybridAssemble($specification); + Specification\ComponentName::normalise($specification, deprecate: false); } // share the token scanner cache ... $this->getAugmenters()->get(Augmenter\Inheritance::class) ?->setAttributeFactory($attributeFactory); + $this->getAugmenters()->setLogger($collecting); $this->getAugmenters()->process($specification); $version = $this->version ?? $specification->openapi->version ?? '3.1.0'; @@ -279,7 +381,12 @@ protected function doBuildSpec(bool $hybrid = false): Result $diagnostics = $compiler->validate($specification); $output = $compiler->compile($specification); - return Result::fromSpec($sourceScanner->getFiles(), $specification, $output, $diagnostics); + return Result::fromSpec( + $sourceScanner->getFiles(), + $specification, + $output, + [...$collecting->entries(), ...$diagnostics], + ); } /** @@ -296,8 +403,7 @@ protected function collectSafely(Assembler $assembler, \ReflectionClass $reflect protected function doHybridAssemble(Specification $specification): void { - $collectingLogger = new CollectingLogger($this->getLogger()); - $generator = new Generator($collectingLogger); + $generator = new Generator($this->getLogger()); if ($this->version !== null) { $generator->setVersion($this->version); @@ -311,10 +417,26 @@ protected function doHybridAssemble(Specification $specification): void $analysis = new Analysis([], new Context([ 'version' => $generator->getVersion(), - 'logger' => $collectingLogger, + 'logger' => $this->getLogger(), ])); $generator->generate($this->sources, $analysis, validate: false); + // the generator scans files only, so a class passed as a reflector is analysed on its own + $analyser = $generator->getAnalyser(); + $sourceScanner = new SourceScanner(new NullLogger()); + $sourceScanner->scan($this->sources); + foreach ($sourceScanner->getReflectors() as $reflector) { + if (!$reflector instanceof \ReflectionClass) { + continue; + } + + if ($analyser instanceof Analysers\ReflectionAnalyser) { + $analyser->fromFqdn($reflector->getName(), $analysis); + } else { + $this->getLogger()->warning(sprintf('Skipping %s for classic attributes: %s reads files only', $reflector->getName(), $analyser::class)); + } + } + $bridge = new HybridBridge(); $bridge->fromAnalysis($analysis, $specification); } @@ -342,21 +464,47 @@ protected function resolveCompiler(string $version): CompilerInterface */ protected function getDefaultAugmenters(): array { + // both runs share one registry, so `withMergers()` reaches them both + $mergers = $this->getMergers(); + return [ + new Augmenter\TypeAliases(), new Augmenter\Inheritance(), + new Augmenter\OperationTags(), + new Augmenter\ImpliedSchemas(), new Augmenter\Names(), new Augmenter\Enums(), new Augmenter\Shortcuts(), + new Augmenter\DefaultMediaType(), + new Augmenter\SecurityRequirements(), new Augmenter\PathItems(), new Augmenter\Types(), + new Augmenter\Properties(), new Augmenter\Refs(), + new Augmenter\Parameters(), + new Augmenter\Merge($mergers), new Augmenter\PathFilter(), new Augmenter\Cleanup(), new Augmenter\MediaTypes(), new Augmenter\Docblocks(), + new Augmenter\OperationIdTemplates(), new Augmenter\OperationIds(), new Augmenter\Tags(), new Augmenter\EnumDescriptions(), + // the same pass again, as the last pipe of all: what a late augmenter added is + // reduced too, so nothing downstream has to be trusted to keep keys unique + new Augmenter\Merge($mergers, Augmenter\Group::Augment), + ]; + } + + /** + * @return list + */ + protected function getDefaultMergers(): array + { + return [ + new Merge\Tags(), + new Merge\LastWins(), ]; } } diff --git a/upload/api/vendor/zircote/swagger-php/src/Builder/Result.php b/upload/api/vendor/zircote/swagger-php/src/Builder/Result.php index 99214cbd..ec383eab 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Builder/Result.php +++ b/upload/api/vendor/zircote/swagger-php/src/Builder/Result.php @@ -7,6 +7,7 @@ namespace OpenApi\Builder; use OpenApi\Annotations\OpenApi; +use OpenApi\Builder; use OpenApi\OpenApiException; use OpenApi\Specification; use Symfony\Component\Yaml\Yaml; @@ -81,11 +82,13 @@ public function isValid(): bool { $generated = $this->openApi instanceof OpenApi || $this->specOutput !== null; - return $generated && $this->errors() === []; + return $generated && !in_array('error', array_column($this->log, 'level'), true); } /** * @return list + * + * @deprecated since 6.16, removed in 8.0 - attach a logger via {@see Builder::setLogger()}; it already receives these */ public function log(): array { @@ -94,6 +97,8 @@ public function log(): array /** * @return list + * + * @deprecated since 6.16, removed in 8.0 - attach a logger via {@see Builder::setLogger()}; it already receives these */ public function warnings(): array { @@ -105,6 +110,8 @@ public function warnings(): array /** * @return list + * + * @deprecated since 6.16, removed in 8.0 - attach a logger via {@see Builder::setLogger()}; it already receives these */ public function errors(): array { @@ -146,7 +153,8 @@ public function toJson(int $flags = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | public function toYaml(int $inline = 10, int $indent = 2): string { if ($this->specOutput !== null) { - return Yaml::dump($this->specOutput, $inline, $indent, Yaml::DUMP_OBJECT_AS_MAP | Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE); + // Dumped from JSON, so keys like `'200'` stay strings and maps stay objects + return Yaml::dump(json_decode($this->toJson()), $inline, $indent, Yaml::DUMP_OBJECT_AS_MAP | Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE); } if (!$this->openApi instanceof OpenApi) { diff --git a/upload/api/vendor/zircote/swagger-php/src/Compiler/OpenApi30Compiler.php b/upload/api/vendor/zircote/swagger-php/src/Compiler/OpenApi30Compiler.php index 5f58d4f9..885dc64b 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Compiler/OpenApi30Compiler.php +++ b/upload/api/vendor/zircote/swagger-php/src/Compiler/OpenApi30Compiler.php @@ -15,15 +15,16 @@ * * Extends the 3.1 compiler and overrides version-specific methods: * - JSON Schema draft-04 subset: type is always a string (not array) - * - nullable is a separate boolean keyword + * - nullable is a separate boolean keyword, which needs a type beside it (3.0.3); a nullable + * $ref, oneOf or anyOf gets a branch that matches only null instead, as 3.1 adds {type: 'null'} * - exclusiveMinimum/Maximum are booleans (used alongside minimum/maximum) - * - No $ref siblings (summary/description stripped from $ref objects) + * - No $ref siblings (summary/description stripped from $ref objects, with a warning) * - No webhooks * - No mutualTLS security scheme (added in 3.1) * - No const, no examples array on Schema (only singular example) * - No prefixItems, unevaluatedItems, unevaluatedProperties * - No if/then/else - * - No contentMediaType/contentEncoding on Schema + * - No contentMediaType/contentEncoding on Schema: base64 becomes format byte, raw binary format binary * - No dependentRequired/dependentSchemas * - No propertyNames, contains, minContains, maxContains * - License: no identifier field (only url) @@ -32,6 +33,11 @@ class OpenApi30Compiler extends OpenApi31Compiler { protected const VERSIONS = ['3.0.0', '3.0.1', '3.0.2', '3.0.3', '3.0.4']; + /** + * A schema that matches only `null`: 3.0 has no `null` type, and `nullable` only takes effect beside a `type`. + */ + protected const NULL_SCHEMA = ['type' => 'object', 'nullable' => true, 'enum' => [null]]; + #[\Override] public function getVersion(): string { @@ -61,6 +67,11 @@ public function validate(Specification $specification): array $this->logger->warning('mutualTLS security schemes are not supported in OpenAPI 3.0 and will be omitted'); } + $hasPathItemComponents = (bool) array_filter($specification->pathItems, fn (OA\PathItem $pathItem): bool => $pathItem->component !== null); + if ($hasPathItemComponents) { + $this->logger->warning('pathItems components are not supported in OpenAPI 3.0 and will be omitted'); + } + if ($specification->info?->license instanceof OA\License) { $license = $specification->info->license; if ($license->identifier !== null) { @@ -68,15 +79,64 @@ public function validate(Specification $specification): array } } + $this->validateReferenceSiblings($specification); + return $this->logger->entries(); } + /** + * 3.0's Reference Object is the `$ref` alone. + */ + #[\Override] + protected function compileReference(OA\Parameter|OA\RequestBody|OA\Response|OA\Header|OA\Link|OA\Example|OA\Security\Scheme $attribute): array + { + return ['$ref' => $attribute->ref]; + } + + protected function validateReferenceSiblings(Specification $specification): void + { + $specification->getWalker()->eachRef(function (object $attribute): void { + if (!$attribute instanceof OA\Parameter && !$attribute instanceof OA\RequestBody && !$attribute instanceof OA\Response + && !$attribute instanceof OA\Header && !$attribute instanceof OA\Link && !$attribute instanceof OA\Example + && !$attribute instanceof OA\Security\Scheme) { + return; + } + + $siblings = array_keys($this->filter([ + 'summary' => $attribute instanceof OA\Example ? $attribute->summary : null, + 'description' => $attribute->description, + ])); + + if ($siblings !== []) { + $this->logger->warning(sprintf( + '%s %s beside $ref %s is not supported in OpenAPI 3.0 and will be omitted in %s', + (new \ReflectionClass($attribute))->getShortName(), + implode(' and ', $siblings), + $this->refString($attribute->ref), + $attribute->getSourceLocation(), + )); + } + }); + } + #[\Override] protected function compileWebhooks(array $operations): array { return []; } + /** + * @return array + */ + #[\Override] + protected function compileComponents(Specification $specification): array + { + $components = parent::compileComponents($specification); + unset($components['pathItems']); + + return $components; + } + #[\Override] protected function compileSecuritySchemes(array $schemes): array { @@ -129,10 +189,8 @@ protected function compileSchema(OA\Schema|string $schema): array|\stdClass if ($schema->ref !== null) { if ($schema->nullable === true) { return $this->filter([ - 'oneOf' => [['$ref' => $schema->ref]], - 'nullable' => true, - 'description' => Undefined::isDefault($schema->description) ? null : $schema->description, - ], $schema); + 'oneOf' => [['$ref' => $schema->ref], self::NULL_SCHEMA], + ], $schema) + $this->compileRefAnnotations($schema); } return ['$ref' => $schema->ref]; @@ -144,6 +202,11 @@ protected function compileSchema(OA\Schema|string $schema): array|\stdClass $type = count($type) === 1 ? $type[0] : null; } + $format = $schema->format ?? $this->contentFormat($schema); + if ($type === null && $schema->format === null && $format !== null) { + $type = 'string'; + } + $nullable = $schema->nullable; if ($nullable === null && $schema->type !== null) { $typeArray = (array) $schema->type; @@ -177,7 +240,7 @@ protected function compileSchema(OA\Schema|string $schema): array|\stdClass $result = $this->filter([ 'type' => $type, - 'format' => $schema->format, + 'format' => $format, 'title' => $schema->title, 'description' => $schema->description, 'nullable' => $nullable, @@ -225,14 +288,23 @@ protected function compileSchema(OA\Schema|string $schema): array|\stdClass 'xml' => $schema->xml instanceof OA\Xml ? $this->compileXml($schema->xml) : null, ], $schema); + // without a type beside it, nullable has no effect; null gets a branch of its own where there is a choice + if (($result['nullable'] ?? false) === true && !isset($result['type'])) { + if (isset($result['oneOf'])) { + $result['oneOf'][] = self::NULL_SCHEMA; + unset($result['nullable']); + } elseif (isset($result['anyOf'])) { + $result['anyOf'][] = self::NULL_SCHEMA; + unset($result['nullable']); + } elseif (!isset($result['allOf'])) { + unset($result['nullable']); + } + } + if ($schema->default !== Undefined::UNDEFINED) { $result['default'] = $schema->default; } - if ($schema->example !== Undefined::UNDEFINED) { - $result['example'] = $schema->example; - } elseif ($schema->examples !== null && $schema->examples !== []) { - $result['example'] = $schema->examples[0]; - } + $result += $this->schemaExample($schema); // const is not supported in 3.0 — fall back to enum if ($schema->const !== Undefined::UNDEFINED && !isset($result['enum'])) { $result['enum'] = [$schema->const]; @@ -241,6 +313,49 @@ protected function compileSchema(OA\Schema|string $schema): array|\stdClass return $result ?: new \stdClass(); } + /** + * 3.0 has the singular `example` only, so `examples` contributes its first value. + * + * @return array + */ + #[\Override] + protected function compileRefAnnotations(OA\Schema $schema): array + { + $result = parent::compileRefAnnotations($schema); + unset($result['example'], $result['examples']); + + return $result + $this->schemaExample($schema); + } + + /** + * The 3.0 format for what 3.1 says with `contentEncoding` and `contentMediaType`. + * + * `base64` is 3.0's `byte`, and a `contentMediaType` on a schema with no `type` is raw + * binary, 3.0's `binary`. A string holding other content has no 3.0 form. + */ + protected function contentFormat(OA\Schema $schema): ?string + { + if ($schema->contentEncoding !== null) { + return $schema->contentEncoding === 'base64' ? 'byte' : null; + } + + return $schema->contentMediaType !== null && $schema->type === null ? 'binary' : null; + } + + /** + * The singular `example`, falling back to the first of `examples`. + * + * @return array + */ + protected function schemaExample(OA\Schema $schema): array + { + if ($schema->example !== Undefined::UNDEFINED) { + return ['example' => $schema->example]; + } + + return $schema->examples !== null && $schema->examples !== [] ? ['example' => $schema->examples[0]] : []; + } + #[\Override] protected function validateSchemas(Specification $specification): void { @@ -254,33 +369,43 @@ protected function validateSchemas(Specification $specification): void } if ($type === 'array' && $schema->items === null) { - $this->logger->warning('Schema' . ($schema->schema ? " \"$schema->schema\"" : '') . ' has type "array" but no items'); + $this->logger->warning('Schema' . $this->schemaLabel($schema) . ' has type "array" but no items'); } $this->validateSchemaType($schema); if ($schema->prefixItems !== null) { - $this->logger->warning('Schema' . ($schema->schema ? " \"$schema->schema\"" : '') . ': prefixItems is not supported in OpenAPI 3.0'); + $this->logger->warning('Schema' . $this->schemaLabel($schema) . ': prefixItems is not supported in OpenAPI 3.0'); } if ($schema->unevaluatedProperties !== null) { - $this->logger->warning('Schema' . ($schema->schema ? " \"$schema->schema\"" : '') . ': unevaluatedProperties is not supported in OpenAPI 3.0'); + $this->logger->warning('Schema' . $this->schemaLabel($schema) . ': unevaluatedProperties is not supported in OpenAPI 3.0'); } if ($schema->unevaluatedItems !== null) { - $this->logger->warning('Schema' . ($schema->schema ? " \"$schema->schema\"" : '') . ': unevaluatedItems is not supported in OpenAPI 3.0'); + $this->logger->warning('Schema' . $this->schemaLabel($schema) . ': unevaluatedItems is not supported in OpenAPI 3.0'); } if ($schema->if instanceof OA\Schema || $schema->then instanceof OA\Schema || $schema->else instanceof OA\Schema) { - $this->logger->warning('Schema' . ($schema->schema ? " \"$schema->schema\"" : '') . ': if/then/else is not supported in OpenAPI 3.0'); + $this->logger->warning('Schema' . $this->schemaLabel($schema) . ': if/then/else is not supported in OpenAPI 3.0'); + } + + if ($schema->contentEncoding !== null || $schema->contentMediaType !== null) { + $format = $this->contentFormat($schema); + if ($format === null || ($schema->format !== null && $schema->format !== $format)) { + $this->logger->warning('Schema' . $this->schemaLabel($schema) . ': contentEncoding/contentMediaType is not supported in OpenAPI 3.0 and will be omitted'); + } } if ($schema->const !== Undefined::UNDEFINED) { - $this->logger->warning('Schema' . ($schema->schema ? " \"$schema->schema\"" : '') . ': const is not supported in OpenAPI 3.0, using enum fallback'); + $this->logger->warning('Schema' . $this->schemaLabel($schema) . ': const is not supported in OpenAPI 3.0, using enum fallback'); } if ($schema->examples !== null) { - $this->logger->warning('Schema' . ($schema->schema ? " \"$schema->schema\"" : '') . ': examples array is not supported in OpenAPI 3.0, using first value as example'); + // a plain 3.0 `$ref` is a Reference Object, which drops everything beside it + $this->logger->warning($schema->ref !== null && $schema->nullable !== true + ? 'Schema' . $this->schemaLabel($schema) . ': examples beside $ref ' . $this->refString($schema->ref) . ' is not supported in OpenAPI 3.0 and will be omitted' + : 'Schema' . $this->schemaLabel($schema) . ': examples array is not supported in OpenAPI 3.0, using first value as example'); } } } diff --git a/upload/api/vendor/zircote/swagger-php/src/Compiler/OpenApi31Compiler.php b/upload/api/vendor/zircote/swagger-php/src/Compiler/OpenApi31Compiler.php index 721ebdc8..d79bf6d9 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Compiler/OpenApi31Compiler.php +++ b/upload/api/vendor/zircote/swagger-php/src/Compiler/OpenApi31Compiler.php @@ -13,6 +13,7 @@ use OpenApi\Specification; use OpenApi\Specification\ComponentName; use OpenApi\Undefined; +use OpenApi\Utils\SecurityRequirement; use Psr\Log\LoggerInterface; /** @@ -22,6 +23,11 @@ class OpenApi31Compiler implements CompilerInterface { protected const VERSIONS = ['3.1.0', '3.1.1', '3.1.2']; + /** + * The methods a path item has an operation field for. + */ + protected const OPERATION_METHODS = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace']; + protected const OPERATION_REQUEST_BODY_METHODS = ['put', 'post', 'delete', 'patch']; /** @@ -95,8 +101,12 @@ public function validate(Specification $specification): array $this->validateResponses($specification); + $this->validateMediaTypes($specification); + $this->validateNames($specification); + $this->validateVersionedComponents($specification); + $this->validateNestedNames($specification); $this->validateSchemaExamples($specification); @@ -113,11 +123,11 @@ public function compile(Specification $specification): array 'info' => $specification->info instanceof OA\Info ? $this->compileInfo($specification->info) : null, 'servers' => array_map($this->compileServer(...), $specification->servers), 'paths' => $this->compilePaths($specification->operations, $specification->pathItems), - 'webhooks' => $this->compileWebhooks($specification->operations), - 'tags' => array_map($this->compileTag(...), $specification->tags), + 'components' => $this->compileComponents($specification), 'security' => $this->compileSecurity($specification->openapi->security ?? []), + 'tags' => array_map($this->compileTag(...), $specification->tags), 'externalDocs' => $specification->externalDocs ? $this->compileExternalDocs($specification->externalDocs[0]) : null, - 'components' => $this->compileComponents($specification), + 'webhooks' => $this->compileWebhooks($specification->operations), ], $specification->openapi); } @@ -220,6 +230,12 @@ protected function compileExternalDocs(OA\ExternalDocumentation $docs): array } /** + * Operations are keyed by path and method, path items by path, and one path holds both. + * + * The union puts a path item's own fields beside the method entries already written for that + * path. Which of two path items for one path survives is `Augmenter\Merge`'s to decide, before + * the compiler sees either. + * * @param list $operations * @param list $pathItems * @return array> @@ -229,7 +245,7 @@ protected function compilePaths(array $operations, array $pathItems = []): array $paths = []; foreach ($operations as $operation) { - if ($operation->path === null || $operation->method === null) { + if ($operation->path === null || !in_array($operation->method, static::OPERATION_METHODS, true)) { continue; } @@ -238,7 +254,7 @@ protected function compilePaths(array $operations, array $pathItems = []): array } foreach ($pathItems as $pathItem) { - if ($pathItem->path === null) { + if ($pathItem->path === null || $pathItem->component !== null) { continue; } @@ -254,6 +270,7 @@ protected function compilePaths(array $operations, array $pathItems = []): array protected function compilePathItem(OA\PathItem $pathItem): array { return $this->filter([ + '$ref' => $pathItem->ref, 'summary' => $pathItem->summary, 'description' => $pathItem->description, 'parameters' => array_map($this->compileParameter(...), $pathItem->parameters ?? []), @@ -270,7 +287,7 @@ protected function compileWebhooks(array $operations): array $webhooks = []; foreach ($operations as $operation) { - if ($operation->webhook === null || $operation->method === null) { + if ($operation->webhook === null || !in_array($operation->method, static::OPERATION_METHODS, true)) { continue; } @@ -286,7 +303,7 @@ protected function compileWebhooks(array $operations): array */ protected function compileOperation(OA\Operation $operation): array { - return $this->filter([ + $result = $this->filter([ 'tags' => $operation->tags, 'summary' => $operation->summary, 'description' => $operation->description, @@ -304,6 +321,14 @@ protected function compileOperation(OA\Operation $operation): array 'security' => $this->compileSecurity($operation->security ?? []), 'servers' => array_map($this->compileServer(...), $operation->servers ?? []), ], $operation); + + // `security: []` opts the operation out of the root requirements, so unlike an unset + // `null` it is a value -- and one filter() cannot tell from unset + if ([] === $operation->security) { + $result['security'] = []; + } + + return $result; } /** @@ -344,7 +369,7 @@ protected function compileCallbackValue(mixed $value): mixed protected function compileParameter(OA\Parameter $parameter): array { if ($parameter->ref !== null) { - return ['$ref' => $parameter->ref]; + return $this->compileReference($parameter); } $result = $this->filter([ @@ -375,7 +400,7 @@ protected function compileRequestBody(OA\RequestBody $body, ?string $method = nu } if ($body->ref !== null) { - return ['$ref' => $body->ref]; + return $this->compileReference($body); } $result = $this->filter([ @@ -402,7 +427,7 @@ protected function compileResponses(array $responses): array protected function compileResponse(OA\Response $response): array { if ($response->ref !== null) { - return ['$ref' => $response->ref]; + return $this->compileReference($response); } return $this->filter([ @@ -419,7 +444,7 @@ protected function compileResponse(OA\Response $response): array protected function compileHeader(OA\Header $header): array { if ($header->ref !== null) { - return ['$ref' => $header->ref]; + return $this->compileReference($header); } $result = $this->filter([ @@ -442,7 +467,10 @@ protected function compileHeader(OA\Header $header): array */ protected function compileMediaTypes(array $mediaTypes): array { - return $this->compileNamedMap($mediaTypes, fn (OA\MediaType $mediaType): string => $mediaType->mediaType ?? 'application/json', $this->compileMediaType(...)); + // one without a type is left out, and reported by validateMediaTypes() + $typed = array_filter($mediaTypes, static fn (OA\MediaType $mediaType): bool => $mediaType->mediaType !== null); + + return $this->compileNamedMap($typed, 'mediaType', $this->compileMediaType(...)); } /** @@ -479,7 +507,7 @@ protected function compileEncoding(OA\Encoding $encoding): array protected function compileLink(OA\Link $link): array { if ($link->ref !== null) { - return ['$ref' => $link->ref]; + return $this->compileReference($link); } $result = $this->filter([ @@ -493,6 +521,21 @@ protected function compileLink(OA\Link $link): array return $this->withDefined($result, ['requestBody' => $link->requestBody]); } + /** + * A Reference Object: the `$ref`, with the `summary` and `description` declared beside it. + * + * @return array + */ + protected function compileReference(OA\Parameter|OA\RequestBody|OA\Response|OA\Header|OA\Link|OA\Example|OA\Security\Scheme $attribute): array + { + // no attribute, so no extensions: a Reference Object takes none + return $this->filter([ + '$ref' => $attribute->ref, + 'summary' => $attribute instanceof OA\Example ? $attribute->summary : null, + 'description' => $attribute->description, + ]); + } + /** * @return array|\stdClass */ @@ -512,13 +555,10 @@ protected function compileSchema(OA\Schema|string $schema): array|\stdClass ]), ['type' => 'null'], ], - ], $schema); + ], $schema) + $this->compileRefAnnotations($schema); } - return $this->filter([ - '$ref' => $schema->ref, - 'description' => $schema->description, - ], $schema); + return $this->filter(['$ref' => $schema->ref], $schema) + $this->compileRefAnnotations($schema); } $type = $schema->type; @@ -602,6 +642,18 @@ protected function compileSchema(OA\Schema|string $schema): array|\stdClass 'xml' => $schema->xml instanceof OA\Xml ? $this->compileXml($schema->xml) : null, ], $schema); + // without a type to add `null` to, null gets a branch of its own where there is a choice + if ($schema->nullable === true && $type === null) { + foreach (['oneOf', 'anyOf'] as $keyword) { + if (isset($result[$keyword])) { + if (!in_array(['type' => 'null'], $result[$keyword], true)) { + $result[$keyword][] = ['type' => 'null']; + } + break; + } + } + } + $result = $this->withDefined($result, [ 'default' => $schema->default, 'const' => $schema->const, @@ -611,6 +663,31 @@ protected function compileSchema(OA\Schema|string $schema): array|\stdClass return $result ?: new \stdClass(); } + /** + * The annotation keywords a `$ref` schema keeps: beside `$ref`, or on the wrapper of a nullable one. + * + * In 3.1 a Schema's `$ref` is an ordinary JSON Schema keyword, so its siblings are evaluated; + * the Reference Object's limit to `summary` and `description` does not apply to it. + * + * @return array + */ + protected function compileRefAnnotations(OA\Schema $schema): array + { + $result = $this->filter([ + 'title' => $schema->title, + 'description' => $schema->description, + 'examples' => $schema->examples, + 'deprecated' => $schema->deprecated, + 'readOnly' => $schema->readOnly, + 'writeOnly' => $schema->writeOnly, + ]); + + return $this->withDefined($result, [ + 'default' => $schema->default, + 'example' => $schema->example, + ]); + } + /** * @param list $properties * @return array @@ -671,9 +748,29 @@ protected function compileComponents(Specification $specification): array 'securitySchemes' => $this->compileSecuritySchemes($specification->securitySchemes), 'links' => $this->compileComponentMap($specification->links, $this->compileLink(...)), 'examples' => $this->compileComponentMap($specification->examples, $this->compileExample(...)), + 'pathItems' => $this->compileComponentMap($specification->pathItems, $this->compilePathItem(...)), + ...$this->compileVersionedComponents($specification), ]); } + /** + * The component buckets a later version adds. 3.1 has none past `pathItems`; 3.2 adds + * `mediaTypes`. Reported, not silently dropped, by `validateVersionedComponents()`. + * + * @return array + */ + protected function compileVersionedComponents(Specification $specification): array + { + return []; + } + + protected function validateVersionedComponents(Specification $specification): void + { + if ($specification->mediaTypes !== []) { + $this->logger->warning('mediaTypes components are not supported before OpenAPI 3.2 and will be omitted'); + } + } + /** * A version that does not support every scheme type filters here; the keying stays shared. * @@ -698,7 +795,7 @@ protected function compileSecurity(array $security): array return $item->toArray(); } - return $item; + return SecurityRequirement::normalise($item); }, $security); } @@ -707,6 +804,10 @@ protected function compileSecurity(array $security): array */ protected function compileSecurityScheme(OA\Security\Scheme $scheme): array { + if ($scheme->ref !== null) { + return $this->compileReference($scheme); + } + return $this->filter([ 'type' => $scheme->type, 'description' => $scheme->description, @@ -755,7 +856,7 @@ protected function compileFlow(OA\Flow $flow): array protected function compileExample(OA\Example $example): array { if ($example->ref !== null) { - return ['$ref' => $example->ref]; + return $this->compileReference($example); } $result = $this->filter([ @@ -772,6 +873,9 @@ protected function compileExample(OA\Example $example): array * be derived from a method or a non-constructor parameter, so an attribute declared there * has to carry its own name — the same requirement classic enforces. `Augmenter\Names` * fills the key in for anything declared on a class, so what reaches here is unnameable. + * + * A key claimed twice is `Augmenter\Merge`'s to reduce and report, and it runs first, so a + * repeat seen here means a `Specification` that was compiled without the pipeline. */ protected function validateNames(Specification $specification): void { @@ -782,6 +886,10 @@ protected function validateNames(Specification $specification): void $name = ComponentName::of($component); if ($name === null) { + if ($component instanceof OA\PathItem) { + continue; // path-bound, keyed by its path under `paths` + } + $this->logger->warning(sprintf( '%s is missing key-field: "%s" in %s', $type, @@ -852,7 +960,7 @@ protected function validateSchemaExamples(Specification $specification): void $this->logger->warning(sprintf( 'Schema%s: examples takes values, not Example objects, in %s', - $schema->schema !== null ? " \"{$schema->schema}\"" : '', + $this->schemaLabel($schema), $example->getSourceLocation(), )); } @@ -866,7 +974,7 @@ protected function validateSchemas(Specification $specification): void foreach ($allSchemas as $schema) { if ($schema->type !== null && (is_array($schema->type) ? in_array('array', $schema->type, true) : $schema->type === 'array')) { if ($schema->items === null && $schema->prefixItems === null && $schema->contains === null) { - $this->logger->warning('Schema' . ($schema->schema ? " \"$schema->schema\"" : '') . ' has type "array" but no items in ' . $schema->getSourceLocation()); + $this->logger->warning('Schema' . $this->schemaLabel($schema) . ' has type "array" but no items in ' . $schema->getSourceLocation()); } } @@ -885,13 +993,27 @@ protected function validateSchemaType(OA\Schema $schema): void continue; } - $this->logger->warning('Schema' . ($schema->schema ? " \"$schema->schema\"" : '') . " has unknown type \"$type\", expecting one of " . implode(', ', static::SCHEMA_TYPES) . ' in ' . $schema->getSourceLocation()); + $this->logger->warning('Schema' . $this->schemaLabel($schema) . " has unknown type \"$type\", expecting one of " . implode(', ', static::SCHEMA_TYPES) . ' in ' . $schema->getSourceLocation()); } } protected function validateOperations(Specification $specification): void { $specification->getWalker()->visit(OA\Operation::class, function (OA\Operation $operation): void { + if ($operation->method === null) { + if ($operation->path !== null || $operation->webhook !== null) { + $this->logger->warning('Operation without a method will be omitted in ' . $operation->getSourceLocation()); + } + + return; + } + + if (!in_array($operation->method, static::OPERATION_METHODS, true)) { + $this->logger->warning("Method {$operation->method} is not supported in OpenAPI " . substr($this->getVersion(), 0, 3) . ' and will be omitted in ' . $operation->getSourceLocation()); + + return; + } + if ($operation->requestBody instanceof OA\RequestBody && !in_array($operation->method, static::OPERATION_REQUEST_BODY_METHODS)) { $this->logger->warning("Request body not supported for method {$operation->method} in " . $operation->getSourceLocation()); } @@ -947,6 +1069,42 @@ protected function validateResponses(Specification $specification): void }); } + /** + * A media type in a `content` list needs its type, which is its key in the document, and + * that key once per list: of two with one type, the later one is kept. + */ + protected function validateMediaTypes(Specification $specification): void + { + $check = function (OA\Response|OA\RequestBody|OA\Parameter|OA\Header $holder): void { + $seen = []; + foreach ($holder->content ?? [] as $mediaType) { + if ($mediaType->mediaType === null) { + $this->logger->warning(sprintf( + '%s is missing key-field: "mediaType" in %s; left out', + (new \ReflectionClass($mediaType))->getShortName(), + $holder->getSourceLocation(), + )); + continue; + } + + if (isset($seen[$mediaType->mediaType])) { + $this->logger->warning(sprintf( + 'Media type "%s" is declared more than once in one content, the last one is kept, in %s', + $mediaType->mediaType, + $holder->getSourceLocation(), + )); + } + $seen[$mediaType->mediaType] = true; + } + }; + + $walker = $specification->getWalker(); + $walker->visit(OA\Response::class, $check); + $walker->visit(OA\RequestBody::class, $check); + $walker->visit(OA\Parameter::class, $check); + $walker->visit(OA\Header::class, $check); + } + /** * @return list */ @@ -1019,10 +1177,31 @@ protected function compileKeyedMap(array $items, string $keyField, \Closure $com * Those have a meaningful fallback; a missing name does not, so it goes to * {@see compileKeyedMap()} instead. * - * @param list $items - * @param string|\Closure $key Property name or fn($item, $index): string * @return array */ + /** + * The schema's key, quoted, for a diagnostic — or nothing, for an inline one. + */ + protected function schemaLabel(OA\Schema $schema): string + { + $name = ComponentName::of($schema); + + return $name !== null ? " \"$name\"" : ''; + } + + /** + * The reference a `$ref` holds, whether it is the string or a `Schema\Ref` around it. + */ + protected function refString(string|OA\Schema\Ref|null $ref): ?string + { + return $ref instanceof OA\Schema\Ref ? $this->refString($ref->ref) : $ref; + } + + /** + * @param array $items + * + * @return array + */ protected function compileNamedMap(array $items, string|\Closure $key, \Closure $compiler): array { $result = []; diff --git a/upload/api/vendor/zircote/swagger-php/src/Compiler/OpenApi32Compiler.php b/upload/api/vendor/zircote/swagger-php/src/Compiler/OpenApi32Compiler.php index 77e44ecb..63a6edb5 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Compiler/OpenApi32Compiler.php +++ b/upload/api/vendor/zircote/swagger-php/src/Compiler/OpenApi32Compiler.php @@ -12,12 +12,16 @@ /** * Compiles a Specification into an OpenAPI 3.2.x document array. * - * 3.2 is a superset of 3.1 — adds Tag summary/parent/kind and PathItem query. + * 3.2 is a superset of 3.1 — adds Tag summary/parent/kind and the QUERY operation, which takes a request body. */ class OpenApi32Compiler extends OpenApi31Compiler { protected const VERSIONS = ['3.2.0']; + protected const OPERATION_METHODS = [...parent::OPERATION_METHODS, 'query']; + + protected const OPERATION_REQUEST_BODY_METHODS = [...parent::OPERATION_REQUEST_BODY_METHODS, 'query']; + public function getVersion(): string { return '3.2.0'; @@ -39,6 +43,22 @@ public function validate(Specification $specification): array return $this->logger->entries(); } + #[\Override] + protected function validateVersionedComponents(Specification $specification): void + { + } + + /** + * @return array + */ + #[\Override] + protected function compileVersionedComponents(Specification $specification): array + { + return [ + 'mediaTypes' => $this->compileComponentMap($specification->mediaTypes, $this->compileMediaType(...)), + ]; + } + /** * @return array */ diff --git a/upload/api/vendor/zircote/swagger-php/src/Console/GenerateCommand.php b/upload/api/vendor/zircote/swagger-php/src/Console/GenerateCommand.php index f083f590..ecfdfc05 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Console/GenerateCommand.php +++ b/upload/api/vendor/zircote/swagger-php/src/Console/GenerateCommand.php @@ -95,16 +95,48 @@ protected function execute(InputInterface $input, OutputInterface $output): int /** * The config keys `--config` accepts; these differ per mode. * - * Mirrors the routing in {@see self::generate()}: spec mode configures the - * augmenter pipeline, classic and hybrid configure the Generator. + * Mirrors the routing in {@see self::splitConfig()}: classic configures the Generator, + * spec the augmenter pipeline, and hybrid both — the Generator only for its own + * `generator.*` keys, since hybrid runs no classic processors. * * @return array */ protected function getDefaultConfig(GenerateInput $input): array { - return $input->mode->isSpec() - ? (new Builder())->getAugmenters()->getConfig() - : (new Generator())->getDefaultConfig(); + return match ($input->mode) { + Builder\Mode::CLASSIC => (new Generator())->getDefaultConfig(), + Builder\Mode::SPEC => (new Builder())->getAugmenters()->getConfig(), + Builder\Mode::HYBRID => ['generator' => (new Generator())->getDefaultConfig()['generator']] + + (new Builder())->getAugmenters()->getConfig(), + }; + } + + /** + * Split `-c` into the Generator's share and the augmenters'. + * + * Hybrid empties the Generator's processor pipeline, so processor keys would reach + * nothing there; everything but `generator.*` goes to the augmenters, which do run. + * + * @return array{0: array, 1: list} generator, augmenters + */ + protected function splitConfig(GenerateInput $input): array + { + if ($input->mode !== Builder\Mode::HYBRID) { + return $input->mode === Builder\Mode::SPEC ? [[], $input->config] : [$input->config, []]; + } + + $generator = []; + $augmenters = []; + foreach ($input->config as $option) { + // each -c is a `key=value` string + if (str_starts_with($option, 'generator.')) { + $generator[] = $option; + } else { + $augmenters[] = $option; + } + } + + return [$generator, $augmenters]; } protected function generate(GenerateInput $input): Result @@ -118,14 +150,14 @@ protected function generate(GenerateInput $input): Result $builder->setVersion($input->version); } - if ($input->config || $input->addProcessor || $input->removeProcessor) { - $builder->withGenerator(function (Generator $generator) use ($input): void { - if ($input->config && $input->mode !== Builder\Mode::SPEC) { - // -c takes `key=value` strings; Generator::setConfig() normalises them - /** @var array $config */ - $config = $input->config; - $generator->setConfig($config); - } + [$generatorConfig, $augmenterConfig] = $this->splitConfig($input); + + if ($generatorConfig !== [] || $input->addProcessor || $input->removeProcessor) { + $builder->withGenerator(function (Generator $generator) use ($input, $generatorConfig): void { + // -c takes `key=value` strings; Generator::setConfig() normalises them + /** @var array $config */ + $config = $generatorConfig; + $generator->setConfig($config); foreach ($input->addProcessor as $processor) { $class = '\OpenApi\Processors\\' . ucfirst((string) $processor); @@ -152,9 +184,9 @@ protected function generate(GenerateInput $input): Result }); } - if ($input->config && $input->mode === Builder\Mode::SPEC) { - $builder->withAugmenters(function (Pipeline $augmenters) use ($input): void { - $augmenters->configure($input->config); + if ($augmenterConfig !== []) { + $builder->withAugmenters(function (Pipeline $augmenters) use ($augmenterConfig): void { + $augmenters->configure($augmenterConfig); }); } diff --git a/upload/api/vendor/zircote/swagger-php/src/Context.php b/upload/api/vendor/zircote/swagger-php/src/Context.php index 7b14dab5..f1816408 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Context.php +++ b/upload/api/vendor/zircote/swagger-php/src/Context.php @@ -62,7 +62,12 @@ public function __construct(array $properties = [], ?Context $parent = null) public function __serialize(): array { - return array_filter(get_object_vars($this), static function ($value): bool { + $properties = get_object_vars($this); + // foreign attributes are for processors during analysis, and nothing reads them from a + // finished document; one holding a closure would make the whole document unserializable + unset($properties['other']); + + return array_filter($properties, static function ($value): bool { $rc = is_object($value) ? new \ReflectionClass($value) : null; return (!$rc instanceof \ReflectionClass || !$rc->isAnonymous()) diff --git a/upload/api/vendor/zircote/swagger-php/src/Contracts/AttributeInterface.php b/upload/api/vendor/zircote/swagger-php/src/Contracts/AttributeInterface.php index 4373a24d..e15ac712 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Contracts/AttributeInterface.php +++ b/upload/api/vendor/zircote/swagger-php/src/Contracts/AttributeInterface.php @@ -6,6 +6,7 @@ namespace OpenApi\Contracts; +use OpenApi\Spec as OA; use OpenApi\Utils\SourceLocation; /** @@ -79,5 +80,30 @@ public function getClassName(): ?string; public function getShortClassName(): ?string; + /** + * Extra data for whoever needs to carry it on an attribute, keyed by whoever writes it. + * + * Nothing in swagger-php writes or reads it. A clone copies the entries; an object stored as a + * value is shared between the clone and the original. + */ + public function getMeta(string $key, mixed $default = null): mixed; + + public function setMeta(string $key, mixed $value): static; + public function getSourceLocation(): SourceLocation; + + /** + * Where this attribute goes when more than one attribute could hold it. `null` is the + * default and behaves as `Fan::Single`. + */ + public function getFan(): ?OA\Fan; + + public function setFan(?OA\Fan $fan): static; + + /** + * A deep copy: every attribute nested in its public properties, in lists and maps too, is + * copied as well, so changing the copy's children leaves the original's alone. The reflector + * and source location are shared; `meta` is copied as `clone` copies it. + */ + public function copy(): static; } diff --git a/upload/api/vendor/zircote/swagger-php/src/Contracts/MergerInterface.php b/upload/api/vendor/zircote/swagger-php/src/Contracts/MergerInterface.php new file mode 100644 index 00000000..6561d84d --- /dev/null +++ b/upload/api/vendor/zircote/swagger-php/src/Contracts/MergerInterface.php @@ -0,0 +1,54 @@ +heldAnnotations($analysis); foreach ($analysis->annotations as $annotation) { if (!$annotation->_context->reflector instanceof \Reflector) { continue; } + $this->reportUnmerged($annotation); + match (true) { $annotation instanceof OA\OpenApi => $this->convertOpenApiMeta($annotation, $specification), $annotation instanceof OA\Info => $specification->info = $this->convertInfo($annotation), @@ -32,17 +35,21 @@ public function fromAnalysis(Analysis $analysis, Specification $specification = $annotation instanceof OA\Tag => $specification->tags[] = $this->convertTag($annotation), $annotation instanceof OA\SecurityScheme && !$annotation->_context->is('nested') => $specification->securitySchemes[] = $this->convertSecurityScheme($annotation), $annotation instanceof OA\PathItem && !$annotation->_context->is('nested') && !Undefined::isDefault($annotation->path) && !($annotation instanceof OA\Webhook) => $this->collectPathItem($annotation, $specification), + $annotation instanceof OA\Webhook && !$annotation->_context->is('nested') && !Undefined::isDefault($annotation->webhook) => $this->convertWebhook($annotation, $specification), $annotation instanceof OA\Components => $this->convertComponents($annotation, $specification), $annotation instanceof OA\Operation && !$annotation->_context->is('nested') => $specification->operations[] = $this->convertOperation($annotation, $this->val($annotation->path), $this->methodFromAnnotation($annotation)), $annotation instanceof OA\Schema && !$this->isNestedSchemaType($annotation) && $annotation->_context->reflector instanceof \ReflectionClass && !$annotation->_context->is('nested') => $classSchemas[$annotation->_context->reflector->getName()][] = $annotation, + // on a method or property, a schema is a component only where it names itself, as in classic + $annotation instanceof OA\Schema && !$this->isNestedSchemaType($annotation) && ($annotation->_context->reflector instanceof \ReflectionMethod || $annotation->_context->reflector instanceof \ReflectionProperty) && !$annotation->_context->is('nested') && !Undefined::isDefault($annotation->schema) => $specification->schemas[] = $this->convertSchema($annotation), $annotation instanceof OA\Property && $this->isClassMember($annotation) => $memberProperties[] = $annotation, $annotation instanceof OA\Response && !$annotation->_context->is('nested') && !Undefined::isDefault($annotation->response) => $specification->responses[] = $this->convertResponse($annotation), $annotation instanceof OA\RequestBody && !$annotation->_context->is('nested') && ($annotation->_context->reflector instanceof \ReflectionClass || !Undefined::isDefault($annotation->request)) => $specification->requestBodies[] = $this->convertRequestBody($annotation), $annotation instanceof OA\Parameter && !$annotation->_context->is('nested') && ($annotation->_context->reflector instanceof \ReflectionClass || !Undefined::isDefault($annotation->parameter)) => $specification->parameters[] = $this->convertParameter($annotation), $annotation instanceof OA\Header && !$annotation->_context->is('nested') && !Undefined::isDefault($annotation->header) => $specification->headers[] = $this->convertHeader($annotation), $annotation instanceof OA\Link && !$annotation->_context->is('nested') => $specification->links[] = $this->convertLink($annotation), + $annotation instanceof OA\Examples && !$annotation->_context->is('nested') && !Undefined::isDefault($annotation->example) => $specification->examples[] = $this->convertExample($annotation), $annotation instanceof OA\ExternalDocumentation && !$annotation->_context->is('nested') => $specification->externalDocs[] = $this->convertExternalDocs($annotation), - default => null, + default => isset($held[spl_object_id($annotation)]) ? null : $this->reportStray($annotation), }; } @@ -53,6 +60,8 @@ public function fromAnalysis(Analysis $analysis, Specification $specification = } } + $this->reportUnadopted($memberProperties, $classSchemas); + $specification->openapi ??= new Spec\OpenApi(); return $specification; @@ -109,6 +118,30 @@ protected function convertSchemaWithMembers(OA\Schema $schema, string $className return $converted; } + /** + * A member property is converted only as part of a class schema that takes it in, so one that + * none takes in is dropped. Say so, in classic's words. + * + * @param list $memberProperties + * @param array> $classSchemas + */ + protected function reportUnadopted(array $memberProperties, array $classSchemas): void + { + $adopting = []; + foreach (array_keys($classSchemas) as $className) { + foreach ($this->getOwnClasses($className, $classSchemas) as $ownClass) { + $adopting[$ownClass] = true; + } + } + + foreach ($memberProperties as $property) { + $declarer = $this->getDeclaringClass($property); + if ($declarer === null || !isset($adopting[$declarer])) { + $this->reportUnexpected($property, $property); + } + } + } + /** * Get the class itself plus non-schema ancestors and interfaces (they contribute properties to this schema). * @@ -182,7 +215,7 @@ protected function convertOpenApiMeta(OA\OpenApi $openApi, Specification $spec): ); if (!Undefined::isDefault($openApi->security)) { - $spec->openapi->security = $this->convertSecurityRequirements($openApi->security); + $spec->openapi->security = $this->convertSecurityRequirements($openApi->security, $openApi); } if (!Undefined::isDefault($openApi->externalDocs)) { @@ -389,7 +422,7 @@ protected function convertOperation(OA\Operation $op, ?string $path, string $met $security = null; if (!Undefined::isDefault($op->security)) { - $security = $this->convertSecurityRequirements($op->security); + $security = $this->convertSecurityRequirements($op->security, $op); } $result = new Spec\Operation( @@ -421,7 +454,6 @@ protected function convertOperation(OA\Operation $op, ?string $path, string $met protected function convertParameter(OA\Parameter $param): Spec\Parameter { $result = new Spec\Parameter( - parameter: $this->val($param->parameter), name: $this->val($param->name), in: $this->val($param->in), description: Undefined::isDefault($param->description) ? Undefined::UNDEFINED : $param->description, @@ -438,9 +470,12 @@ protected function convertParameter(OA\Parameter $param): Spec\Parameter ? null : array_values(array_map($this->convertExample(...), $param->examples)), content: $this->resolveContent($param), + component: $this->val($param->parameter), x: $this->extensions($param), ); $this->copyReflector($param, $result); + // classic requires a parameter by its type alone, so spec's default rule leaves this one alone + $result->setMeta(self::class, true); return $result; } @@ -480,11 +515,11 @@ protected function convertResponse(OA\Response $response): Spec\Response protected function convertRequestBody(OA\RequestBody $body): Spec\RequestBody { $result = new Spec\RequestBody( - request: $this->val($body->request), description: $this->val($body->description), required: $this->val($body->required), ref: $this->val($body->ref), content: $this->resolveContent($body), + component: $this->val($body->request), x: $this->extensions($body), ); $this->copyReflector($body, $result); @@ -514,6 +549,83 @@ protected function convertMediaType(OA\MediaType $mediaType): Spec\MediaType ); } + /** + * The annotations another one holds in a property, which convert with their holder. A stacked + * attribute merged into its sibling is held without being marked nested. + * + * @return array keyed by `spl_object_id()` + */ + protected function heldAnnotations(Analysis $analysis): array + { + $held = []; + foreach ($analysis->annotations as $annotation) { + foreach (get_object_vars($annotation) as $property => $value) { + if ($property === '_unmerged') { + continue; + } + foreach (is_array($value) ? $value : [$value] as $item) { + if ($item instanceof OA\AbstractAnnotation) { + $held[spl_object_id($item)] = true; + } + } + } + } + + return $held; + } + + /** + * A top-level annotation none of the arms converts and nothing holds is dropped, so say so, in classic's words. + */ + protected function reportStray(OA\AbstractAnnotation $annotation): void + { + if ($annotation->_context->is('nested') || !$annotation::$_parents) { + return; + } + + $this->reportUnexpected($annotation, $annotation); + } + + protected function reportUnexpected(OA\AbstractAnnotation $reporter, OA\AbstractAnnotation $annotation): void + { + $message = 'Unexpected ' . $annotation->identity(); + if ($annotation::$_parents) { + $message .= ', expected to be inside ' . implode(', ', array_map( + static fn (string $class): string => '@' . str_replace(['OpenApi\\Annotations\\', 'OpenApi\\Attributes\\'], 'OA\\', $class), + $annotation::$_parents, + )); + } + $reporter->_context->logger?->warning($message . ' in ' . $annotation->_context); + } + + /** + * An annotation left unmerged where it was declared is not converted, so say so, in classic's words. + * + * Classic reports these from `validate()`, which hybrid does not run. JSON and XML content on a + * parent that takes content is the exception: {@see self::resolveContent()} converts it. + */ + protected function reportUnmerged(OA\AbstractAnnotation $parent): void + { + foreach ($parent->_unmerged as $annotation) { + if (!$annotation instanceof OA\AbstractAnnotation) { + continue; + } + + if (($annotation instanceof OA\JsonContent || $annotation instanceof OA\XmlContent) + && ($parent instanceof OA\Response || $parent instanceof OA\RequestBody || $parent instanceof OA\Parameter || $parent instanceof OA\Header)) { + continue; + } + + if ($parent->matchNested($annotation) !== null) { + $parent->_context->logger?->warning('Only one ' . $annotation->identity([]) . ' allowed for ' . $parent->identity() . ' multiple found, skipped: ' . $annotation->_context); + + continue; + } + + $this->reportUnexpected($parent, $annotation); + } + } + /** * @return list|null */ @@ -655,7 +767,6 @@ protected function convertSchema(OA\Schema $schema): Spec\Schema } $result = new Spec\Schema( - schema: $this->val($schema->schema), title: $this->val($schema->title), description: Undefined::isDefault($schema->description) ? Undefined::UNDEFINED : $schema->description, ref: $this->val($schema->ref) ?? $this->typeAsRef($schema->type), @@ -720,9 +831,12 @@ enum: Undefined::isDefault($schema->enum) ? null : (is_string($schema->enum) ? [ ? null : $this->convertExternalDocs($schema->externalDocs), xml: Undefined::isDefault($schema->xml) ? null : $this->convertXml($schema->xml), + component: $this->val($schema->schema), x: $this->extensions($schema), ); $this->copyReflector($schema, $result); + // classic infers nothing from the declarations, so spec's inference leaves this schema alone + $result->setMeta(self::class, true); return $result; } @@ -818,7 +932,6 @@ protected function convertSecurityScheme(OA\SecurityScheme $scheme): Spec\Securi } $result = new Spec\Security\Scheme( - securityScheme: $this->val($scheme->securityScheme), type: $this->val($scheme->type), description: $this->val($scheme->description), name: $this->val($scheme->name), @@ -828,6 +941,7 @@ protected function convertSecurityScheme(OA\SecurityScheme $scheme): Spec\Securi openIdConnectUrl: $this->val($scheme->openIdConnectUrl), flows: $flows, ref: $this->val($scheme->ref), + component: $this->val($scheme->securityScheme), x: $this->extensions($scheme), ); $this->copyReflector($scheme, $result); @@ -866,16 +980,21 @@ protected function convertExample(OA\Examples $example): Spec\Example } /** - * @param array $security classic security blocks: each entry is a scheme => scopes map + * @param array $security classic security blocks: each entry is a scheme => scopes map, + * or a bare scheme name, which classic reads as that scheme with no scopes * * @return list */ - protected function convertSecurityRequirements(array $security): array + protected function convertSecurityRequirements(array $security, OA\AbstractAnnotation $owner): array { $requirements = []; foreach ($security as $item) { if (is_array($item)) { $requirements[] = new Spec\Security\Requirement(schemes: $item); + } elseif (is_string($item)) { + // kept, as in classic, and reported, since spec drops it + $owner->_context->logger?->warning(sprintf('Security requirement "%s" is a bare scheme name, use [\'%s\' => []] in %s', $item, $item, $owner->_context)); + $requirements[] = new Spec\Security\Requirement(scheme: $item); } } @@ -1029,8 +1148,18 @@ protected function extensions(OA\AbstractAnnotation $annotation): ?array return is_array($annotation->x) ? $annotation->x : null; } + /** + * A property takes the reflector only when its own context holds it. + * + * A nested property's context inherits the reflector of the member that declares its + * parent, and that member's docblock describes the member, not the property. + */ protected function copyReflector(OA\AbstractAnnotation $from, Spec\AbstractAttribute $to): void { + if ($from instanceof OA\Property && !$from->_context->is('reflector')) { + return; + } + if ($from->_context->reflector !== null && $from->_context->reflector instanceof \Reflector) { $to->setReflector($from->_context->reflector); } diff --git a/upload/api/vendor/zircote/swagger-php/src/Merge/Concerns/FoldTrait.php b/upload/api/vendor/zircote/swagger-php/src/Merge/Concerns/FoldTrait.php new file mode 100644 index 00000000..44120f47 --- /dev/null +++ b/upload/api/vendor/zircote/swagger-php/src/Merge/Concerns/FoldTrait.php @@ -0,0 +1,116 @@ + $fields + * @param list $conflicts + */ + protected function fold(object $survivor, object $later, array $fields, Mode $mode, array &$conflicts, string $prefix = ''): void + { + foreach ($fields as $field) { + $value = $later->{$field}; + if (self::isUnset($value)) { + continue; + } + + $current = $survivor->{$field}; + if (self::isUnset($current)) { + $survivor->{$field} = $value; + continue; + } + + if ($current != $value) { + $conflicts[] = $prefix . $field; + if ($mode === Mode::Last) { + $survivor->{$field} = $value; + } + } + } + } + + /** + * @template T + * + * @param list|null $earlier + * @param list|null $later + * + * @return list|null + */ + protected function union(?array $earlier, ?array $later): ?array + { + if ($later === null) { + return $earlier; + } + if ($earlier === null) { + return $later; + } + + $result = $earlier; + foreach ($later as $item) { + if (!in_array($item, $result, true)) { + $result[] = $item; + } + } + + return $result; + } + + /** + * Key by key: a key only one side has is taken, and one both have differently is a conflict + * resolved by the mode. + * + * @param array|null $earlier + * @param array|null $later + * @param list $conflicts + * + * @return array|null + */ + protected function extensions(?array $earlier, ?array $later, Mode $mode, array &$conflicts, string $prefix = ''): ?array + { + if ($later === null) { + return $earlier; + } + if ($earlier === null) { + return $later; + } + + $result = $earlier; + foreach ($later as $key => $value) { + if (!array_key_exists($key, $result)) { + $result[$key] = $value; + continue; + } + + if ($result[$key] != $value) { + $conflicts[] = $prefix . 'x.' . $key; + if ($mode === Mode::Last) { + $result[$key] = $value; + } + } + } + + return $result; + } + + protected static function isUnset(mixed $value): bool + { + return $value === null || Undefined::isDefault($value); + } +} diff --git a/upload/api/vendor/zircote/swagger-php/src/Merge/Concerns/IdentityTrait.php b/upload/api/vendor/zircote/swagger-php/src/Merge/Concerns/IdentityTrait.php new file mode 100644 index 00000000..9621e91f --- /dev/null +++ b/upload/api/vendor/zircote/swagger-php/src/Merge/Concerns/IdentityTrait.php @@ -0,0 +1,100 @@ +method === null) { + return null; + } + + return match (true) { + $attribute->path !== null => 'operation:' . $attribute->method . ' ' . $attribute->path, + $attribute->webhook !== null => 'webhook:' . $attribute->method . ' ' . $attribute->webhook, + default => null, + }; + } + + if ($attribute instanceof OA\Tag) { + return $attribute->name !== null ? 'tag:' . $attribute->name : null; + } + + if (ComponentName::isComponentType($attribute)) { + $component = ComponentName::of($attribute); + if ($component !== null) { + return 'component:' . $component; + } + + // a path item without a component key is path-bound, and keyed by its path + return $attribute instanceof OA\PathItem && $attribute->path !== null + ? 'path:' . $attribute->path + : null; + } + + return null; + } + + /** + * Both are named, because either could be the one the author did not mean to write. Two + * contributed attributes both report `unknown`, which is stated once. + */ + protected function collision(AttributeInterface $earlier, AttributeInterface $later, string $outcome = 'keeping the last'): string + { + $earlierAt = (string) $earlier->getSourceLocation(); + $laterAt = (string) $later->getSourceLocation(); + + return sprintf( + '%s "%s" is declared more than once, %s in %s', + (new \ReflectionClass($later))->getShortName(), + $this->label($later), + $outcome, + $earlierAt === $laterAt ? $laterAt : $laterAt . ' and ' . $earlierAt, + ); + } + + /** + * What the key reads as in a message. The identity string is a grouping key and carries a + * space prefix nobody needs to see. + */ + protected function label(AttributeInterface $attribute): string + { + if ($attribute instanceof OA\Operation) { + return trim(($attribute->method ?? '') . ' ' . ($attribute->path ?? $attribute->webhook ?? '')); + } + + if ($attribute instanceof OA\Tag) { + return (string) $attribute->name; + } + + return (string) (ComponentName::of($attribute) ?? ($attribute instanceof OA\PathItem ? $attribute->path : null)); + } +} diff --git a/upload/api/vendor/zircote/swagger-php/src/Merge/Concerns/ModeTrait.php b/upload/api/vendor/zircote/swagger-php/src/Merge/Concerns/ModeTrait.php new file mode 100644 index 00000000..8d438639 --- /dev/null +++ b/upload/api/vendor/zircote/swagger-php/src/Merge/Concerns/ModeTrait.php @@ -0,0 +1,45 @@ +mode = $mode instanceof Mode ? $mode : \Closure::fromCallable($mode); + + return $this; + } + + protected function modeFor(AttributeInterface $earlier, AttributeInterface $later): Mode + { + if ($this->mode instanceof Mode) { + return $this->mode; + } + + $mode = ($this->mode)($earlier, $later); + if (!$mode instanceof Mode) { + throw new \UnexpectedValueException(sprintf('The mode callable of %s must return a %s, got %s', static::class, Mode::class, get_debug_type($mode))); + } + + return $mode; + } +} diff --git a/upload/api/vendor/zircote/swagger-php/src/Merge/LastWins.php b/upload/api/vendor/zircote/swagger-php/src/Merge/LastWins.php new file mode 100644 index 00000000..bd9dc25c --- /dev/null +++ b/upload/api/vendor/zircote/swagger-php/src/Merge/LastWins.php @@ -0,0 +1,40 @@ +logger?->warning($this->collision($earlier, $later)); + + return $later; + } +} diff --git a/upload/api/vendor/zircote/swagger-php/src/Merge/Mode.php b/upload/api/vendor/zircote/swagger-php/src/Merge/Mode.php new file mode 100644 index 00000000..409b9fc5 --- /dev/null +++ b/upload/api/vendor/zircote/swagger-php/src/Merge/Mode.php @@ -0,0 +1,42 @@ + 'folding into the first', + self::Last => 'folding into the last', + self::NonOverlapping => 'taking only what does not overlap', + }; + } +} diff --git a/upload/api/vendor/zircote/swagger-php/src/Merge/Operations.php b/upload/api/vendor/zircote/swagger-php/src/Merge/Operations.php new file mode 100644 index 00000000..3d07a9c7 --- /dev/null +++ b/upload/api/vendor/zircote/swagger-php/src/Merge/Operations.php @@ -0,0 +1,205 @@ + $mergers->insert(new Merge\Operations(), Merge\LastWins::class))`, + * when one operation is described in more than one place, such as a route table contributed + * through `withSpecification()` and an attribute adding responses to it. + * + * Identity is the shared one: path and method, or webhook and method. Registering the fold + * changes how two operations combine, not which two are the same. + * + * What only one of them sets is always taken. What both set is decided by the `Mode` it is + * constructed with, `Mode::Last` by default: fold into the later, fold into the earlier, or + * take only what does not overlap. The mode can also be a callable that receives both and returns + * one, so the choice can follow a mark a producer left with `setMeta()` rather than the + * order they arrive in. A field both set differently is reported, whichever value + * is kept. One they set the same is not. + * + * - Parameters are keyed by name and location, and responses by status code. Under `First` and + * `Last`, a parameter or response both describe is folded one level deep, a parameter's + * `schema` included, so a route's `pattern` survives an attribute's `type`. Under + * `NonOverlapping` it is the earlier's, whole. + * - `requestBody`, `externalDocs`, `security`, `servers` and `callbacks` are each one decision, + * taken whole from whichever operation the mode keeps. An explicit `security: []` is set, + * and opts out. + * - `tags` and `attachables` are unions, and `x` merges key by key under the same mode. + * + * The survivor is a new operation, a shallow copy of the earlier with the later folded in, so + * neither operation is changed. What it takes whole, such as a response or a request body, is + * the contribution's own object, and the augmenters that run later change it in place, as they + * would without the merge. It keeps the earlier's reflector, source location and `meta`, + * falling back to the later's reflector when the earlier has none. + */ +class Operations implements MergerInterface, LoggerAwareInterface +{ + use FoldTrait; + use IdentityTrait; + use LoggerAwareTrait; + use ModeTrait; + + /** + * @param Mode|callable(AttributeInterface, AttributeInterface): Mode $mode + */ + public function __construct(Mode|callable $mode = Mode::Last) + { + $this->setMode($mode); + } + + public function supports(string $class): bool + { + return is_a($class, OA\Operation::class, true); + } + + public function merge(AttributeInterface $earlier, AttributeInterface $later): AttributeInterface + { + // supports() claims operations only; anything else is kept as the catch-all would keep it + if (!$earlier instanceof OA\Operation || !$later instanceof OA\Operation) { + $this->logger?->warning($this->collision($earlier, $later)); + + return $later; + } + + $mode = $this->modeFor($earlier, $later); + + // shallow: the survivor replaces both, so no child it shares ends up with two holders + $survivor = clone $earlier; + if (!$survivor->getReflector() instanceof \Reflector && $later->getReflector() instanceof \Reflector) { + $survivor->setReflector($later->getReflector()); + } + + $conflicts = []; + + $this->fold($survivor, $later, ['operationId', 'summary', 'description', 'deprecated', 'requestBody', 'externalDocs', 'security', 'servers', 'callbacks'], $mode, $conflicts); + + $survivor->tags = $this->union($survivor->tags, $later->tags); + $survivor->attachables = $this->union($survivor->attachables, $later->attachables); + $survivor->x = $this->extensions($survivor->x, $later->x, $mode, $conflicts); + $survivor->parameters = $this->keyed($survivor->parameters, $later->parameters, $this->parameterKey(...), 'parameter', $mode, $conflicts); + $survivor->responses = $this->keyed($survivor->responses, $later->responses, $this->responseKey(...), 'response', $mode, $conflicts); + + if ($conflicts !== []) { + $this->logger?->warning(sprintf( + '%s; %s set differently', + $this->collision($earlier, $later, $mode->outcome()), + implode(', ', array_unique($conflicts)), + )); + } + + return $survivor; + } + + /** + * One level deep: the public fields of two parameters or two responses, and of a + * parameter's schema, folded by the same rule. Deeper structure is taken whole. + * + * @param list $conflicts + */ + protected function foldChild(OA\AbstractAttribute $earlier, OA\AbstractAttribute $later, string $label, Mode $mode, array &$conflicts): OA\AbstractAttribute + { + $survivor = clone $earlier; + + $fields = array_keys(array_filter( + get_object_vars($later), + static fn (string $field): bool => !in_array($field, ['x', 'attachables', 'schema'], true), + ARRAY_FILTER_USE_KEY, + )); + $this->fold($survivor, $later, $fields, $mode, $conflicts, $label . '.'); + + $survivor->attachables = $this->union($survivor->attachables, $later->attachables); + $survivor->x = $this->extensions($survivor->x, $later->x, $mode, $conflicts, $label . '.'); + + if ($survivor instanceof OA\Parameter && $later instanceof OA\Parameter) { + if ($survivor->schema instanceof OA\Schema && $later->schema instanceof OA\Schema) { + $schema = clone $survivor->schema; + $this->fold($schema, $later->schema, array_keys(get_object_vars($later->schema)), $mode, $conflicts, $label . '.schema.'); + $survivor->schema = $schema; + } elseif ($later->schema instanceof OA\Schema) { + $survivor->schema = $later->schema; + } + } + + return $survivor; + } + + /** + * Entries keyed by `$key` are combined: a key only one side has is taken, and a key both + * have is folded with `foldChild()`, or under `Mode::NonOverlapping` left as the earlier + * side's, with the fields that differ still reported. + * + * @template T of OA\AbstractAttribute + * + * @param list|null $earlier + * @param list|null $later + * @param callable(T): (string|null) $key null for an entry with no identity, which is kept as it is + * @param list $conflicts + * + * @return list|null + */ + protected function keyed(?array $earlier, ?array $later, callable $key, string $label, Mode $mode, array &$conflicts): ?array + { + if ($later === null || $later === []) { + return $earlier; + } + if ($earlier === null || $earlier === []) { + return $later; + } + + $result = []; + $slots = []; + foreach ([...$earlier, ...$later] as $item) { + $identity = $item instanceof OA\AbstractAttribute ? $key($item) : null; + if ($identity === null) { + $result[] = $item; + continue; + } + + if (!array_key_exists($identity, $slots)) { + $slots[$identity] = count($result); + $result[] = $item; + continue; + } + + /** @var T $folded */ + $folded = $this->foldChild($result[$slots[$identity]], $item, $label . ' ' . $identity, $mode, $conflicts); + if ($mode !== Mode::NonOverlapping) { + $result[$slots[$identity]] = $folded; + } + } + + // replacing a slot keeps the list in order; the analyser only loses track that it is one + return array_values($result); + } + + protected function parameterKey(OA\AbstractAttribute $parameter): ?string + { + return $parameter instanceof OA\Parameter && $parameter->name !== null && $parameter->in !== null + ? $parameter->in . ':' . $parameter->name + : null; + } + + protected function responseKey(OA\AbstractAttribute $response): ?string + { + return $response instanceof OA\Response && $response->response !== null + ? (string) $response->response + : null; + } +} diff --git a/upload/api/vendor/zircote/swagger-php/src/Merge/Tags.php b/upload/api/vendor/zircote/swagger-php/src/Merge/Tags.php new file mode 100644 index 00000000..a0d6fbdc --- /dev/null +++ b/upload/api/vendor/zircote/swagger-php/src/Merge/Tags.php @@ -0,0 +1,83 @@ +setMode($mode); + } + + public function supports(string $class): bool + { + return is_a($class, OA\Tag::class, true); + } + + public function merge(AttributeInterface $earlier, AttributeInterface $later): AttributeInterface + { + // supports() claims tags only; anything else is kept as the catch-all would keep it + if (!$earlier instanceof OA\Tag || !$later instanceof OA\Tag) { + $this->logger?->warning($this->collision($earlier, $later)); + + return $later; + } + + $mode = $this->modeFor($earlier, $later); + + // a shallow copy is enough: both tags give way to the survivor, so what it shares has one holder + $survivor = clone $earlier; + $conflicts = []; + + $this->fold($survivor, $later, ['summary', 'description', 'externalDocs', 'parent', 'kind'], $mode, $conflicts); + + $survivor->attachables = $this->union($survivor->attachables, $later->attachables); + $survivor->x = $this->extensions($survivor->x, $later->x, $mode, $conflicts); + + if ($conflicts !== []) { + $this->logger?->warning(sprintf( + '%s; %s set differently', + $this->collision($earlier, $later, $mode->outcome()), + implode(', ', array_unique($conflicts)), + )); + } + + return $survivor; + } +} diff --git a/upload/api/vendor/zircote/swagger-php/src/Resolver.php b/upload/api/vendor/zircote/swagger-php/src/Resolver.php index 2cc49b4e..c4861c73 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Resolver.php +++ b/upload/api/vendor/zircote/swagger-php/src/Resolver.php @@ -12,6 +12,9 @@ use OpenApi\Spec as OA; use OpenApi\Specification\ComponentIndex; use OpenApi\Utils\TypedList; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerAwareTrait; +use Psr\Log\LoggerInterface; /** * Finds and resolves FQCNs referenced by the specification that have no corresponding schema. @@ -20,8 +23,10 @@ * 1. Ref values that are raw FQCNs (not yet rewritten to `#/components/...` paths) * 2. Property/parameter type hints on schema class reflectors */ -class Resolver +class Resolver implements LoggerAwareInterface { + use LoggerAwareTrait; + protected const MAX_ITERATIONS = 50; /** @@ -57,7 +62,8 @@ public function withResolvers(callable $hook): static /** * Resolve all found unresolved FQCN in the specification assembled so far. * - * The first resolver to claim success will resolve the FQCN. + * The first resolver to claim success will resolve the FQCN. A resolver implementing + * `LoggerAwareInterface` is given this resolver's logger first. */ public function resolve(Assembler $assembler): void { @@ -65,6 +71,12 @@ public function resolve(Assembler $assembler): void return; } + foreach ($this->resolvers as $resolver) { + if ($resolver instanceof LoggerAwareInterface && $this->logger instanceof LoggerInterface) { + $resolver->setLogger($this->logger); + } + } + $specification = $assembler->getSpecification(); // FQCN no resolver was able to handle; not worth another attempt in this run @@ -84,7 +96,17 @@ public function resolve(Assembler $assembler): void } foreach ($this->resolvers as $resolver) { - if ($resolver->resolve($fqcn, $assembler)) { + try { + $claimed = $resolver->resolve($fqcn, $assembler); + } catch (\Throwable $throwable) { + // as the scan does for a class it cannot load: report it, and resolve the rest + $this->logger?->warning("Skipping unloadable {$fqcn}: {$throwable->getMessage()}"); + $attempted[$fqcn] = true; + + continue 2; + } + + if ($claimed) { $resolved = true; continue 2; } diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/AbstractAttribute.php b/upload/api/vendor/zircote/swagger-php/src/Spec/AbstractAttribute.php index 63a0e36e..47c9fcae 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/AbstractAttribute.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/AbstractAttribute.php @@ -15,14 +15,24 @@ abstract class AbstractAttribute implements AttributeInterface protected ?SourceLocation $sourceLocation = null; + /** + * @var array + */ + protected array $meta = []; + + protected ?Fan $fan = null; + /** * @param array|null $x * @param list|null $attachables Reusable custom attachable attributes + * @param Fan|null $fan Where this attribute goes when more than one attribute could hold it */ public function __construct( public ?array $x = null, public ?array $attachables = null, + ?Fan $fan = null, ) { + $this->fan = $fan; } public function isRoot(): bool @@ -90,6 +100,41 @@ public function setReflector(?\Reflector $reflector): static return $this; } + public function getFan(): ?Fan + { + return $this->fan; + } + + public function setFan(?Fan $fan): static + { + $this->fan = $fan; + + return $this; + } + + public function copy(): static + { + $copy = clone $this; + + foreach (get_object_vars($copy) as $property => $value) { + $copy->{$property} = self::copyValue($value); + } + + return $copy; + } + + public function getMeta(string $key, mixed $default = null): mixed + { + return array_key_exists($key, $this->meta) ? $this->meta[$key] : $default; + } + + public function setMeta(string $key, mixed $value): static + { + $this->meta[$key] = $value; + + return $this; + } + public function getSourceLocation(): SourceLocation { if (!$this->sourceLocation instanceof SourceLocation) { @@ -123,4 +168,25 @@ protected static function wrapList(mixed $value): ?array return $list; } + + /** + * A bare schema stands for one media type with that payload and no `mediaType` yet. + * + * @param MediaType|list|Schema|null $content + * + * @return list|null + */ + protected static function wrapContent(MediaType|Schema|array|null $content): ?array + { + return $content instanceof Schema ? [new MediaType(schema: $content)] : self::wrapList($content); + } + + private static function copyValue(mixed $value): mixed + { + if ($value instanceof AttributeInterface) { + return $value->copy(); + } + + return is_array($value) ? array_map(self::copyValue(...), $value) : $value; + } } diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Components.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Components.php index 8955186e..5a55e2a5 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/Components.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Components.php @@ -13,14 +13,17 @@ * of the OpenAPI document. The Components attribute itself is not emitted — its children * are promoted to their respective Specification buckets. * - * The primary use case is for DTOs that are NOT roots and therefore cannot be declared - * at class level on their own: Parameter, Header, Link, and Example. Other types - * (Schema, PathItem, SecurityScheme, named Response/RequestBody) are already roots and - * can be declared directly on a class without needing a Components wrapper. + * Any attribute given a `component:` key is a root on its own and needs no wrapper. The + * wrapper remains for the historic spellings — a Header or Example keyed by `header`/`example` + * — and for grouping several components on one class. + * + * On a class it also makes a holder: each member declares a component of its own, and one + * without a key is named after the member, the way a class names its own. A component declared + * on a member needs the holder; without it, a keyed one is left out with a warning. * * #[Components] * class SharedComponents { - * #[Parameter(parameter: 'tenant_id', name: 'tenant_id', in: 'path', schema: new Schema(type: 'string'))] + * #[Parameter(component: 'tenant_id', name: 'tenant_id', in: 'path', schema: new Schema(type: 'string'))] * public string $tenantId; * * #[Header(header: 'X-Rate-Limit', schema: new Schema(type: 'integer'))] @@ -42,8 +45,11 @@ class Components extends AbstractAttribute * @param list $securitySchemes * @param list $links * @param list $examples + * @param list $pathItems + * @param list $mediaTypes OpenAPI 3.2; earlier compilers omit them with a warning * @param array|null $x Vendor extensions (x-* properties) * @param list|null $attachables Reusable custom attachable attributes + * @param Fan|null $fan Where this attribute goes when more than one attribute could hold it */ public function __construct( public array $schemas = [], @@ -54,10 +60,13 @@ public function __construct( public array $securitySchemes = [], public array $links = [], public array $examples = [], + public array $pathItems = [], + public array $mediaTypes = [], ?array $x = null, ?array $attachables = null, + ?Fan $fan = null, ) { - parent::__construct(x: $x, attachables: $attachables); + parent::__construct(x: $x, attachables: $attachables, fan: $fan); } public function isRoot(): bool diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Concerns/SchemaInputTrait.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Concerns/SchemaInputTrait.php new file mode 100644 index 00000000..8fe11578 --- /dev/null +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Concerns/SchemaInputTrait.php @@ -0,0 +1,85 @@ +|null + */ + protected static function typeValue(string|Type|array|null $type): string|array|null + { + if (!is_array($type)) { + return $type instanceof Type ? $type->value : $type; + } + + return array_values(array_map(static fn (string|Type $type): string => $type instanceof Type ? $type->value : $type, $type)); + } + + /** + * Turns keyed `properties` into the list of `Property` the model stores. + * + * A key names the property only where its value is a `Schema`, a `Type` or a `Format`, so a + * list of `Property` passes through as it always has. A `Format` brings the type it implies. + * A `Schema`, `Type` or `Format` without a string key has no name, and is an error. + * + * @param PropertiesInput|null $properties + * + * @return list|null + * + * @throws OpenApiException for a `Schema`, `Type` or `Format` without a string key + */ + protected static function propertyList(?array $properties): ?array + { + if ($properties === null) { + return null; + } + + $list = []; + foreach ($properties as $key => $value) { + if ($value instanceof Property) { + $list[] = $value; + continue; + } + + if (!is_string($key)) { + throw new OpenApiException(sprintf( + 'properties: %s needs a string key that names the property', + $value instanceof Schema ? $value::class : $value::class . '::' . $value->name, + )); + } + + $schema = match (true) { + $value instanceof Type => new Schema(type: $value), + $value instanceof Format => new Schema(type: $value->type(), format: $value), + default => $value, + }; + + $list[] = new Property(property: $key, schema: $schema); + } + + return $list; + } +} diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Contact.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Contact.php index b2702255..4c91c3f9 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/Contact.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Contact.php @@ -20,6 +20,7 @@ class Contact extends AbstractAttribute * @param string|null $email The email address of the contact person/organization * @param array|null $x Vendor extensions (x-* properties) * @param list|null $attachables Reusable custom attachable attributes + * @param Fan|null $fan Where this attribute goes when more than one attribute could hold it */ public function __construct( public ?string $name = null, @@ -27,8 +28,9 @@ public function __construct( public ?string $email = null, ?array $x = null, ?array $attachables = null, + ?Fan $fan = null, ) { - parent::__construct(x: $x, attachables: $attachables); + parent::__construct(x: $x, attachables: $attachables, fan: $fan); } public function merge(): array diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Discriminator.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Discriminator.php index c8f0ed88..8413ab4b 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/Discriminator.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Discriminator.php @@ -20,14 +20,16 @@ class Discriminator extends AbstractAttribute * @param array|null $mapping Maps payload values to schema names or references * @param array|null $x Vendor extensions (x-* properties) * @param list|null $attachables Reusable custom attachable attributes + * @param Fan|null $fan Where this attribute goes when more than one attribute could hold it */ public function __construct( public ?string $propertyName = null, public ?array $mapping = null, ?array $x = null, ?array $attachables = null, + ?Fan $fan = null, ) { - parent::__construct(x: $x, attachables: $attachables); + parent::__construct(x: $x, attachables: $attachables, fan: $fan); } public function merge(): array diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Encoding.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Encoding.php index 79378ad2..d96c174e 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/Encoding.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Encoding.php @@ -25,6 +25,7 @@ class Encoding extends AbstractAttribute * @param bool|null $allowReserved Whether reserved characters are allowed without encoding * @param array|null $x Vendor extensions (x-* properties) * @param list|null $attachables Reusable custom attachable attributes + * @param Fan|null $fan Where this attribute goes when more than one attribute could hold it */ public function __construct( public ?string $encoding = null, @@ -35,8 +36,9 @@ public function __construct( public ?bool $allowReserved = null, ?array $x = null, ?array $attachables = null, + ?Fan $fan = null, ) { - parent::__construct(x: $x, attachables: $attachables); + parent::__construct(x: $x, attachables: $attachables, fan: $fan); $this->style = $style instanceof \BackedEnum ? $style->value : $style; } diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Example.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Example.php index 1e666a34..400d44fb 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/Example.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Example.php @@ -16,15 +16,20 @@ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY | \Attribute::TARGET_PARAMETER | \Attribute::IS_REPEATABLE)] class Example extends AbstractAttribute { + /** The key this attribute is filed under in `components`; null for an inline one. */ + public ?string $component = null; + /** - * @param string|null $example Reusable example identifier (component key) + * @param string|null $example The example name — the key the example nests under in a media type, parameter or header * @param string|null $summary Short description of the example * @param string|null $description Long description of the example (CommonMark syntax) * @param mixed $value Embedded literal example value * @param string|null $externalValue A URI pointing to the literal example * @param string|null $ref A JSON Reference to a reusable example + * @param string|null $component The key this is filed under in `components`, which makes it a reusable component * @param array|null $x Vendor extensions (x-* properties) * @param list|null $attachables Reusable custom attachable attributes + * @param Fan|null $fan Where this attribute goes when more than one attribute could hold it */ public function __construct( public ?string $example = null, @@ -33,10 +38,18 @@ public function __construct( public mixed $value = Undefined::UNDEFINED, public ?string $externalValue = null, public string|Schema\Ref|null $ref = null, + ?string $component = null, ?array $x = null, ?array $attachables = null, + ?Fan $fan = null, ) { - parent::__construct(x: $x, attachables: $attachables); + parent::__construct(x: $x, attachables: $attachables, fan: $fan); + $this->component = $component; + } + + public function isRoot(): bool + { + return $this->component !== null; } public function merge(): array @@ -52,6 +65,7 @@ public function merge(): array public function contained(): array { return [ + Components::class => 'examples[]', MediaType::class => 'examples[]', Parameter::class => 'examples[]', Header::class => 'examples[]', diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/ExternalDocumentation.php b/upload/api/vendor/zircote/swagger-php/src/Spec/ExternalDocumentation.php index 58f137c7..f993f89a 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/ExternalDocumentation.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/ExternalDocumentation.php @@ -19,14 +19,16 @@ class ExternalDocumentation extends AbstractAttribute * @param string|null $description A description of the target documentation (CommonMark syntax) * @param array|null $x Vendor extensions (x-* properties) * @param list|null $attachables Reusable custom attachable attributes + * @param Fan|null $fan Where this attribute goes when more than one attribute could hold it */ public function __construct( public ?string $url = null, public ?string $description = null, ?array $x = null, ?array $attachables = null, + ?Fan $fan = null, ) { - parent::__construct(x: $x, attachables: $attachables); + parent::__construct(x: $x, attachables: $attachables, fan: $fan); } public function isRoot(): bool diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Fan.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Fan.php new file mode 100644 index 00000000..bed220d3 --- /dev/null +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Fan.php @@ -0,0 +1,21 @@ +|null $scopes The available scopes for the OAuth2 security scheme * @param array|null $x Vendor extensions (x-* properties) * @param list|null $attachables Reusable custom attachable attributes + * @param Fan|null $fan Where this attribute goes when more than one attribute could hold it */ public function __construct( string|FlowType|null $flow = null, @@ -61,8 +62,9 @@ public function __construct( public ?array $scopes = null, ?array $x = null, ?array $attachables = null, + ?Fan $fan = null, ) { - parent::__construct(x: $x, attachables: $attachables); + parent::__construct(x: $x, attachables: $attachables, fan: $fan); $this->flow = $flow instanceof \BackedEnum ? $flow->value : $flow; } diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Flow/AuthorizationCode.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Flow/AuthorizationCode.php index 466eca0e..82bd7f2c 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/Flow/AuthorizationCode.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Flow/AuthorizationCode.php @@ -11,6 +11,27 @@ /** * Configuration for the OAuth2 Authorization Code flow. * + * Allows to shorten this: + * + * ```php + * new OA\Flow( + * flow: 'authorizationCode', + * authorizationUrl: 'https://example.com/oauth/authorize', + * tokenUrl: 'https://example.com/oauth/token', + * scopes: ['read:pets' => 'Read your pets'], + * ) + * ``` + * + * to this: + * + * ```php + * new OA\Flow\AuthorizationCode( + * authorizationUrl: 'https://example.com/oauth/authorize', + * tokenUrl: 'https://example.com/oauth/token', + * scopes: ['read:pets' => 'Read your pets'], + * ) + * ``` + * * @see [OAuth Flow Object](https://spec.openapis.org/oas/v3.1.1.html#oauth-flow-object) */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)] @@ -28,6 +49,7 @@ public function __construct( ?array $scopes = null, ?array $x = null, ?array $attachables = null, + ?OA\Fan $fan = null, ) { parent::__construct( flow: OA\FlowType::AuthorizationCode, @@ -37,6 +59,7 @@ public function __construct( scopes: $scopes, x: $x, attachables: $attachables, + fan: $fan, ); } } diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Flow/ClientCredentials.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Flow/ClientCredentials.php index a9ba0fb7..88ea8ff6 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/Flow/ClientCredentials.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Flow/ClientCredentials.php @@ -11,6 +11,25 @@ /** * Configuration for the OAuth2 Client Credentials flow. * + * Allows to shorten this: + * + * ```php + * new OA\Flow( + * flow: 'clientCredentials', + * tokenUrl: 'https://example.com/oauth/token', + * scopes: ['read:pets' => 'Read your pets'], + * ) + * ``` + * + * to this: + * + * ```php + * new OA\Flow\ClientCredentials( + * tokenUrl: 'https://example.com/oauth/token', + * scopes: ['read:pets' => 'Read your pets'], + * ) + * ``` + * * @see [OAuth Flow Object](https://spec.openapis.org/oas/v3.1.1.html#oauth-flow-object) */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)] @@ -27,6 +46,7 @@ public function __construct( ?array $scopes = null, ?array $x = null, ?array $attachables = null, + ?OA\Fan $fan = null, ) { parent::__construct( flow: OA\FlowType::ClientCredentials, @@ -35,6 +55,7 @@ public function __construct( scopes: $scopes, x: $x, attachables: $attachables, + fan: $fan, ); } } diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Flow/Implicit.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Flow/Implicit.php index b0521dc0..c338edc4 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/Flow/Implicit.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Flow/Implicit.php @@ -11,6 +11,25 @@ /** * Configuration for the OAuth2 Implicit flow. * + * Allows to shorten this: + * + * ```php + * new OA\Flow( + * flow: 'implicit', + * authorizationUrl: 'https://example.com/oauth/authorize', + * scopes: ['read:pets' => 'Read your pets'], + * ) + * ``` + * + * to this: + * + * ```php + * new OA\Flow\Implicit( + * authorizationUrl: 'https://example.com/oauth/authorize', + * scopes: ['read:pets' => 'Read your pets'], + * ) + * ``` + * * @see [OAuth Flow Object](https://spec.openapis.org/oas/v3.1.1.html#oauth-flow-object) */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)] @@ -27,6 +46,7 @@ public function __construct( ?array $scopes = null, ?array $x = null, ?array $attachables = null, + ?OA\Fan $fan = null, ) { parent::__construct( flow: OA\FlowType::Implicit, @@ -35,6 +55,7 @@ public function __construct( scopes: $scopes, x: $x, attachables: $attachables, + fan: $fan, ); } } diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Flow/Password.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Flow/Password.php index 320ea1ce..4acf3c35 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/Flow/Password.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Flow/Password.php @@ -11,6 +11,25 @@ /** * Configuration for the OAuth2 Resource Owner Password flow. * + * Allows to shorten this: + * + * ```php + * new OA\Flow( + * flow: 'password', + * tokenUrl: 'https://example.com/oauth/token', + * scopes: ['read:pets' => 'Read your pets'], + * ) + * ``` + * + * to this: + * + * ```php + * new OA\Flow\Password( + * tokenUrl: 'https://example.com/oauth/token', + * scopes: ['read:pets' => 'Read your pets'], + * ) + * ``` + * * @see [OAuth Flow Object](https://spec.openapis.org/oas/v3.1.1.html#oauth-flow-object) */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)] @@ -27,6 +46,7 @@ public function __construct( ?array $scopes = null, ?array $x = null, ?array $attachables = null, + ?OA\Fan $fan = null, ) { parent::__construct( flow: OA\FlowType::Password, @@ -35,6 +55,7 @@ public function __construct( scopes: $scopes, x: $x, attachables: $attachables, + fan: $fan, ); } } diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Format.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Format.php new file mode 100644 index 00000000..da8a523d --- /dev/null +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Format.php @@ -0,0 +1,59 @@ + Type::Integer, + self::Float, self::Double => Type::Number, + default => Type::String, + }; + } +} diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Header.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Header.php index 28b3912b..2cd84541 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/Header.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Header.php @@ -16,13 +16,16 @@ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Header extends AbstractAttribute { + /** The key this attribute is filed under in `components`; null for an inline one. */ + public ?string $component = null; + public ?string $style = null; /** @var list|null */ public ?array $content = null; /** - * @param string|null $header The header name (component key) + * @param string|null $header The header name — the key the header nests under in a response or an encoding * @param string|null $description A brief description of the header (CommonMark syntax) * @param bool|null $required Whether the header is mandatory * @param bool|null $deprecated Whether the header is deprecated @@ -33,8 +36,10 @@ class Header extends AbstractAttribute * @param mixed $example Example of the header's value * @param list|null $examples Examples of the header's value * @param MediaType|list|null $content Content-type based header serialization + * @param string|null $component The key this is filed under in `components`, which makes it a reusable component * @param array|null $x Vendor extensions (x-* properties) * @param list|null $attachables Reusable custom attachable attributes + * @param Fan|null $fan Where this attribute goes when more than one attribute could hold it */ public function __construct( public ?string $header = null, @@ -48,14 +53,22 @@ public function __construct( public mixed $example = Undefined::UNDEFINED, public ?array $examples = null, MediaType|array|null $content = null, + ?string $component = null, ?array $x = null, ?array $attachables = null, + ?Fan $fan = null, ) { - parent::__construct(x: $x, attachables: $attachables); + parent::__construct(x: $x, attachables: $attachables, fan: $fan); + $this->component = $component; $this->style = $style instanceof \BackedEnum ? $style->value : $style; $this->content = self::wrapList($content); } + public function isRoot(): bool + { + return $this->component !== null; + } + public function merge(): array { return [ @@ -68,6 +81,7 @@ public function merge(): array public function contained(): array { return [ + Components::class => 'headers[]', Response::class => 'headers[]', ]; } diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/HttpMethod.php b/upload/api/vendor/zircote/swagger-php/src/Spec/HttpMethod.php index 11f2ce27..163fc98a 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/HttpMethod.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/HttpMethod.php @@ -16,4 +16,5 @@ enum HttpMethod: string case Head = 'head'; case Patch = 'patch'; case Trace = 'trace'; + case Query = 'query'; } diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Info.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Info.php index e14ea241..3b58c770 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/Info.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Info.php @@ -24,6 +24,7 @@ class Info extends AbstractAttribute * @param string|null $summary A short summary of the API * @param array|null $x Vendor extensions (x-* properties) * @param list|null $attachables Reusable custom attachable attributes + * @param Fan|null $fan Where this attribute goes when more than one attribute could hold it */ public function __construct( public ?string $title = null, @@ -35,8 +36,9 @@ public function __construct( public ?string $summary = null, ?array $x = null, ?array $attachables = null, + ?Fan $fan = null, ) { - parent::__construct(x: $x, attachables: $attachables); + parent::__construct(x: $x, attachables: $attachables, fan: $fan); } public function isRoot(): bool diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/License.php b/upload/api/vendor/zircote/swagger-php/src/Spec/License.php index 348a3ace..779f2324 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/License.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/License.php @@ -20,6 +20,7 @@ class License extends AbstractAttribute * @param string|null $url A URL to the license used for the API * @param array|null $x Vendor extensions (x-* properties) * @param list|null $attachables Reusable custom attachable attributes + * @param Fan|null $fan Where this attribute goes when more than one attribute could hold it */ public function __construct( public ?string $name = null, @@ -27,8 +28,9 @@ public function __construct( public ?string $url = null, ?array $x = null, ?array $attachables = null, + ?Fan $fan = null, ) { - parent::__construct(x: $x, attachables: $attachables); + parent::__construct(x: $x, attachables: $attachables, fan: $fan); } public function merge(): array diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Link.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Link.php index ef95e310..485abbae 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/Link.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Link.php @@ -16,8 +16,11 @@ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Link extends AbstractAttribute { + /** The key this attribute is filed under in `components`; null for an inline one. */ + public ?string $component = null; + /** - * @param string|null $link Reusable link identifier (component key) + * @param string|null $link The link name — the key the link nests under in a response * @param string|null $operationRef A relative or absolute URI reference to a linked operation * @param string|null $operationId The name of an existing operation (mutually exclusive with operationRef) * @param array|null $parameters Values to pass to the linked operation's parameters @@ -25,8 +28,10 @@ class Link extends AbstractAttribute * @param string|null $description A description of the link (CommonMark syntax) * @param string|null $ref A JSON Reference to a reusable link * @param Server|null $server A server object to be used by the target operation + * @param string|null $component The key this is filed under in `components`, which makes it a reusable component * @param array|null $x Vendor extensions (x-* properties) * @param list|null $attachables Reusable custom attachable attributes + * @param Fan|null $fan Where this attribute goes when more than one attribute could hold it */ public function __construct( public ?string $link = null, @@ -37,15 +42,18 @@ public function __construct( public ?string $description = null, public string|Schema\Ref|null $ref = null, public ?Server $server = null, + ?string $component = null, ?array $x = null, ?array $attachables = null, + ?Fan $fan = null, ) { - parent::__construct(x: $x, attachables: $attachables); + parent::__construct(x: $x, attachables: $attachables, fan: $fan); + $this->component = $component; } public function isRoot(): bool { - return $this->link !== null && $this->ref === null; + return $this->ref === null && ($this->component !== null || $this->link !== null); } public function merge(): array @@ -59,6 +67,7 @@ public function merge(): array public function contained(): array { return [ + Components::class => 'links[]', Response::class => 'links[]', ]; } diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/MediaType.php b/upload/api/vendor/zircote/swagger-php/src/Spec/MediaType.php index 38dc0a27..244287c9 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/MediaType.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/MediaType.php @@ -16,14 +16,19 @@ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class MediaType extends AbstractAttribute { + /** The key this attribute is filed under in `components`; null for an inline one. */ + public ?string $component = null; + /** * @param string|null $mediaType The media type identifier (e.g. 'application/json') * @param Schema|null $schema The schema defining the content * @param mixed $example Example of the media type content * @param list|null $examples Examples of the media type content * @param list|array|null $encoding Encoding information for specific properties + * @param string|null $component The key this is filed under in `components`, which makes it a reusable component * @param array|null $x Vendor extensions (x-* properties) * @param list|null $attachables Reusable custom attachable attributes + * @param Fan|null $fan Where this attribute goes when more than one attribute could hold it */ public function __construct( public ?string $mediaType = null, @@ -31,15 +36,24 @@ public function __construct( public mixed $example = Undefined::UNDEFINED, public ?array $examples = null, public ?array $encoding = null, + ?string $component = null, ?array $x = null, ?array $attachables = null, + ?Fan $fan = null, ) { - parent::__construct(x: $x, attachables: $attachables); + parent::__construct(x: $x, attachables: $attachables, fan: $fan); + $this->component = $component; + } + + public function isRoot(): bool + { + return $this->component !== null; } public function merge(): array { return [ + Components::class => 'mediaTypes[]', Response::class => 'content[]', RequestBody::class => 'content[]', Parameter::class => 'content[]', diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/MediaType/Json.php b/upload/api/vendor/zircote/swagger-php/src/Spec/MediaType/Json.php index d13005dd..a1576035 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/MediaType/Json.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/MediaType/Json.php @@ -7,8 +7,10 @@ namespace OpenApi\Spec\MediaType; use OpenApi\Spec as OA; +use OpenApi\Spec\Concerns\SchemaInputTrait; use OpenApi\Spec\Property; use OpenApi\Spec\Schema; +use OpenApi\Spec\Type; use OpenApi\Undefined; /** @@ -21,48 +23,67 @@ * * Allows to shorten this: * - * #[OA\Response(response: 200, content: [ - * new OA\MediaType(mediaType: 'application/json', schema: new OA\Schema(type: 'array', items: new OA\Schema(ref: Pet::class))), - * ])] + * ```php + * #[OA\Response(response: 200, content: [ + * new OA\MediaType(mediaType: 'application/json', schema: new OA\Schema(type: 'array', items: new OA\Schema(ref: Pet::class))), + * ])] + * ``` * * to this: * - * #[OA\Response(response: 200, content: [new OA\MediaType\Json(type: 'array', items: new OA\Schema(ref: Pet::class))])] + * ```php + * #[OA\Response(response: 200, content: [new OA\MediaType\Json(type: 'array', items: new OA\Schema(ref: Pet::class))])] + * ``` * * The `Shortcuts` augmenter expands the schema properties into a nested `OA\Schema` automatically. * * @see [Media Type Object](https://spec.openapis.org/oas/v3.1.1.html#media-type-object) + * + * @phpstan-import-type TypeInput from Schema + * @phpstan-import-type PropertiesInput from Schema */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Json extends OA\MediaType { + use SchemaInputTrait; + + /** @var string|list|null */ + public string|array|null $type = null; + + /** @var list|null */ + public ?array $properties = null; + /** * @param string|null $ref A JSON Reference to a reusable schema - * @param string|list|null $type The value type(s) (string, number, integer, boolean, array, object, null) + * @param TypeInput|null $type The value type(s) (string, number, integer, boolean, array, object, null) * @param Schema|string|null $items Schema for array items - * @param list|null $properties Object property definitions + * @param PropertiesInput|null $properties Object property definitions, as a list of `Property` or keyed by property name * @param list|null $required List of required property names * @param Schema|null $schema The schema defining the content * @param mixed $example Example of the media type content * @param list|null $examples Examples of the media type content * @param list|array|null $encoding Encoding information for specific properties + * @param string|null $component The key this is filed under in `components`, which makes it a reusable component * @param array|null $x Vendor extensions (x-* properties) * @param list|null $attachables Reusable custom attachable attributes + * @param OA\Fan|null $fan Where this attribute goes when more than one attribute could hold it */ public function __construct( // schema shortcuts public string|Schema\Ref|null $ref = null, - public string|array|null $type = null, + string|Type|array|null $type = null, public Schema|string|null $items = null, - public ?array $properties = null, + ?array $properties = null, public ?array $required = null, // media type ?Schema $schema = null, mixed $example = Undefined::UNDEFINED, ?array $examples = null, ?array $encoding = null, + ?string $component = null, ?array $x = null, ?array $attachables = null, + ?OA\Fan $fan = null, ) { parent::__construct( mediaType: 'application/json', @@ -70,8 +91,12 @@ public function __construct( example: $example, examples: $examples, encoding: $encoding, + component: $component, x: $x, - attachables: $attachables + attachables: $attachables, + fan: $fan, ); + $this->type = self::typeValue($type); + $this->properties = self::propertyList($properties); } } diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/MediaType/Xml.php b/upload/api/vendor/zircote/swagger-php/src/Spec/MediaType/Xml.php index 4c171ac2..2d7405d9 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/MediaType/Xml.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/MediaType/Xml.php @@ -7,8 +7,10 @@ namespace OpenApi\Spec\MediaType; use OpenApi\Spec as OA; +use OpenApi\Spec\Concerns\SchemaInputTrait; use OpenApi\Spec\Property; use OpenApi\Spec\Schema; +use OpenApi\Spec\Type; use OpenApi\Undefined; /** @@ -21,48 +23,67 @@ * * Allows to shorten this: * - * #[OA\Response(response: 200, content: [ - * new OA\MediaType(mediaType: 'application/xml', schema: new OA\Schema(type: 'array', items: new OA\Schema(ref: Pet::class))), - * ])] + * ```php + * #[OA\Response(response: 200, content: [ + * new OA\MediaType(mediaType: 'application/xml', schema: new OA\Schema(type: 'array', items: new OA\Schema(ref: Pet::class))), + * ])] + * ``` * * to this: * - * #[OA\Response(response: 200, content: [new OA\MediaType\Xml(type: 'array', items: new OA\Schema(ref: Pet::class))])] + * ```php + * #[OA\Response(response: 200, content: [new OA\MediaType\Xml(type: 'array', items: new OA\Schema(ref: Pet::class))])] + * ``` * * The `Shortcuts` augmenter expands the schema properties into a nested `OA\Schema` automatically. * * @see [Media Type Object](https://spec.openapis.org/oas/v3.1.1.html#media-type-object) + * + * @phpstan-import-type TypeInput from Schema + * @phpstan-import-type PropertiesInput from Schema */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Xml extends OA\MediaType { + use SchemaInputTrait; + + /** @var string|list|null */ + public string|array|null $type = null; + + /** @var list|null */ + public ?array $properties = null; + /** * @param string|null $ref A JSON Reference to a reusable schema - * @param string|list|null $type The value type(s) (string, number, integer, boolean, array, object, null) + * @param TypeInput|null $type The value type(s) (string, number, integer, boolean, array, object, null) * @param Schema|string|null $items Schema for array items - * @param list|null $properties Object property definitions + * @param PropertiesInput|null $properties Object property definitions, as a list of `Property` or keyed by property name * @param list|null $required List of required property names * @param Schema|null $schema The schema defining the content * @param mixed $example Example of the media type content * @param list|null $examples Examples of the media type content * @param list|array|null $encoding Encoding information for specific properties + * @param string|null $component The key this is filed under in `components`, which makes it a reusable component * @param array|null $x Vendor extensions (x-* properties) * @param list|null $attachables Reusable custom attachable attributes + * @param OA\Fan|null $fan Where this attribute goes when more than one attribute could hold it */ public function __construct( // schema shortcuts public string|Schema\Ref|null $ref = null, - public string|array|null $type = null, + string|Type|array|null $type = null, public Schema|string|null $items = null, - public ?array $properties = null, + ?array $properties = null, public ?array $required = null, // media type ?Schema $schema = null, mixed $example = Undefined::UNDEFINED, ?array $examples = null, ?array $encoding = null, + ?string $component = null, ?array $x = null, ?array $attachables = null, + ?OA\Fan $fan = null, ) { parent::__construct( mediaType: 'application/xml', @@ -70,8 +91,12 @@ public function __construct( example: $example, examples: $examples, encoding: $encoding, + component: $component, x: $x, - attachables: $attachables + attachables: $attachables, + fan: $fan, ); + $this->type = self::typeValue($type); + $this->properties = self::propertyList($properties); } } diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/OpenApi.php b/upload/api/vendor/zircote/swagger-php/src/Spec/OpenApi.php index 3e4f6118..30e43e13 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/OpenApi.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/OpenApi.php @@ -19,14 +19,16 @@ class OpenApi extends AbstractAttribute * @param list|null $security Default security requirements for the API * @param array|null $x Vendor extensions (x-* properties) * @param list|null $attachables Reusable custom attachable attributes + * @param Fan|null $fan Where this attribute goes when more than one attribute could hold it */ public function __construct( public ?string $version = null, public ?array $security = null, ?array $x = null, ?array $attachables = null, + ?Fan $fan = null, ) { - parent::__construct(x: $x, attachables: $attachables); + parent::__construct(x: $x, attachables: $attachables, fan: $fan); } public function isRoot(): bool diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Operation.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Operation.php index f3b293b1..6be84c20 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/Operation.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Operation.php @@ -45,10 +45,15 @@ class Operation extends AbstractAttribute { public ?string $method = null; + /** + * @var list|null + */ + public ?array $methods = null; + /** * @param string|null $path The URL path for the operation * @param string|null $webhook The webhook name (mutually exclusive with path) - * @param string|HttpMethod|null $method The HTTP method (get, post, put, delete, etc.) + * @param string|HttpMethod|null $method The HTTP method (get, post, put, delete, etc.). An operation still without one at compile time is left out, with a warning * @param string|null $operationId Unique identifier for the operation * @param string|null $summary A short summary of what the operation does * @param string|null $description A verbose explanation of the operation (CommonMark syntax) @@ -63,6 +68,8 @@ class Operation extends AbstractAttribute * @param ExternalDocumentation|null $externalDocs Additional external documentation * @param array|null $x Vendor extensions (x-* properties) * @param list|null $attachables Reusable custom attachable attributes + * @param Fan|null $fan Where this attribute goes when more than one attribute could hold it + * @param list|null $methods Several HTTP methods for one declaration, instead of `method`. Split into one operation per method during assembly */ public function __construct( public ?string $path = null, @@ -82,13 +89,21 @@ public function __construct( public ?ExternalDocumentation $externalDocs = null, ?array $x = null, ?array $attachables = null, + ?Fan $fan = null, + ?array $methods = null, ) { - parent::__construct(x: $x, attachables: $attachables); - $this->method = $method instanceof \BackedEnum ? $method->value : strtolower($method); + parent::__construct(x: $x, attachables: $attachables, fan: $fan); + $this->method = $method === null ? null : self::normaliseMethod($method); + $this->methods = $methods === null ? null : array_values(array_unique(array_map(self::normaliseMethod(...), $methods))); } public function isRoot(): bool { return true; } + + protected static function normaliseMethod(string|HttpMethod $method): string + { + return $method instanceof HttpMethod ? $method->value : strtolower($method); + } } diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Operation/Delete.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Operation/Delete.php index 4ba870eb..e8f788e2 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/Operation/Delete.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Operation/Delete.php @@ -12,6 +12,18 @@ /** * Shorthand for an HTTP DELETE operation. * + * Allows to shorten this: + * + * ```php + * #[OA\Operation(method: 'delete', path: '/pets/{id}')] + * ``` + * + * to this: + * + * ```php + * #[OA\Operation\Delete(path: '/pets/{id}')] + * ``` + * * @see [Operation Object](https://spec.openapis.org/oas/v3.1.1.html#operation-object) */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] @@ -44,6 +56,7 @@ public function __construct( ?OA\ExternalDocumentation $externalDocs = null, ?array $x = null, ?array $attachables = null, + ?OA\Fan $fan = null, ) { parent::__construct( path: $path, @@ -63,6 +76,7 @@ public function __construct( externalDocs: $externalDocs, x: $x, attachables: $attachables, + fan: $fan, ); } } diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Operation/Get.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Operation/Get.php index 52ed64c5..def1aea4 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/Operation/Get.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Operation/Get.php @@ -15,6 +15,18 @@ /** * Shorthand for an HTTP GET operation. * + * Allows to shorten this: + * + * ```php + * #[OA\Operation(method: 'get', path: '/pets')] + * ``` + * + * to this: + * + * ```php + * #[OA\Operation\Get(path: '/pets')] + * ``` + * * @see [Operation Object](https://spec.openapis.org/oas/v3.1.1.html#operation-object) */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] @@ -46,6 +58,7 @@ public function __construct( ?OA\ExternalDocumentation $externalDocs = null, ?array $x = null, ?array $attachables = null, + ?OA\Fan $fan = null, ) { parent::__construct( path: $path, @@ -64,6 +77,7 @@ public function __construct( externalDocs: $externalDocs, x: $x, attachables: $attachables, + fan: $fan, ); } } diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Operation/Head.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Operation/Head.php index 14f1b2e8..bffa87f1 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/Operation/Head.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Operation/Head.php @@ -12,6 +12,18 @@ /** * Shorthand for an HTTP HEAD operation. * + * Allows to shorten this: + * + * ```php + * #[OA\Operation(method: 'head', path: '/pets')] + * ``` + * + * to this: + * + * ```php + * #[OA\Operation\Head(path: '/pets')] + * ``` + * * @see [Operation Object](https://spec.openapis.org/oas/v3.1.1.html#operation-object) */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] @@ -43,6 +55,7 @@ public function __construct( ?OA\ExternalDocumentation $externalDocs = null, ?array $x = null, ?array $attachables = null, + ?OA\Fan $fan = null, ) { parent::__construct( path: $path, @@ -61,6 +74,7 @@ public function __construct( externalDocs: $externalDocs, x: $x, attachables: $attachables, + fan: $fan, ); } } diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Operation/Options.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Operation/Options.php index 58ec9674..4fd6942f 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/Operation/Options.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Operation/Options.php @@ -12,6 +12,18 @@ /** * Shorthand for an HTTP OPTIONS operation. * + * Allows to shorten this: + * + * ```php + * #[OA\Operation(method: 'options', path: '/pets')] + * ``` + * + * to this: + * + * ```php + * #[OA\Operation\Options(path: '/pets')] + * ``` + * * @see [Operation Object](https://spec.openapis.org/oas/v3.1.1.html#operation-object) */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] @@ -43,6 +55,7 @@ public function __construct( ?OA\ExternalDocumentation $externalDocs = null, ?array $x = null, ?array $attachables = null, + ?OA\Fan $fan = null, ) { parent::__construct( path: $path, @@ -61,6 +74,7 @@ public function __construct( externalDocs: $externalDocs, x: $x, attachables: $attachables, + fan: $fan, ); } } diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Operation/Patch.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Operation/Patch.php index 0da4a2ba..64f4ae8f 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/Operation/Patch.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Operation/Patch.php @@ -12,6 +12,18 @@ /** * Shorthand for an HTTP PATCH operation. * + * Allows to shorten this: + * + * ```php + * #[OA\Operation(method: 'patch', path: '/pets/{id}')] + * ``` + * + * to this: + * + * ```php + * #[OA\Operation\Patch(path: '/pets/{id}')] + * ``` + * * @see [Operation Object](https://spec.openapis.org/oas/v3.1.1.html#operation-object) */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] @@ -44,6 +56,7 @@ public function __construct( ?OA\ExternalDocumentation $externalDocs = null, ?array $x = null, ?array $attachables = null, + ?OA\Fan $fan = null, ) { parent::__construct( path: $path, @@ -63,6 +76,7 @@ public function __construct( externalDocs: $externalDocs, x: $x, attachables: $attachables, + fan: $fan, ); } } diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Operation/Post.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Operation/Post.php index d707760d..b73a70e7 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/Operation/Post.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Operation/Post.php @@ -12,6 +12,18 @@ /** * Shorthand for an HTTP POST operation. * + * Allows to shorten this: + * + * ```php + * #[OA\Operation(method: 'post', path: '/pets')] + * ``` + * + * to this: + * + * ```php + * #[OA\Operation\Post(path: '/pets')] + * ``` + * * @see [Operation Object](https://spec.openapis.org/oas/v3.1.1.html#operation-object) */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] @@ -44,6 +56,7 @@ public function __construct( ?OA\ExternalDocumentation $externalDocs = null, ?array $x = null, ?array $attachables = null, + ?OA\Fan $fan = null, ) { parent::__construct( path: $path, @@ -63,6 +76,7 @@ public function __construct( externalDocs: $externalDocs, x: $x, attachables: $attachables, + fan: $fan, ); } } diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Operation/Put.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Operation/Put.php index d453f55f..7e8cb64b 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/Operation/Put.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Operation/Put.php @@ -12,6 +12,18 @@ /** * Shorthand for an HTTP PUT operation. * + * Allows to shorten this: + * + * ```php + * #[OA\Operation(method: 'put', path: '/pets/{id}')] + * ``` + * + * to this: + * + * ```php + * #[OA\Operation\Put(path: '/pets/{id}')] + * ``` + * * @see [Operation Object](https://spec.openapis.org/oas/v3.1.1.html#operation-object) */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] @@ -44,6 +56,7 @@ public function __construct( ?OA\ExternalDocumentation $externalDocs = null, ?array $x = null, ?array $attachables = null, + ?OA\Fan $fan = null, ) { parent::__construct( path: $path, @@ -63,6 +76,7 @@ public function __construct( externalDocs: $externalDocs, x: $x, attachables: $attachables, + fan: $fan, ); } } diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Operation/Trace.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Operation/Trace.php index 5501f013..caee24f1 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/Operation/Trace.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Operation/Trace.php @@ -12,6 +12,18 @@ /** * Shorthand for an HTTP TRACE operation. * + * Allows to shorten this: + * + * ```php + * #[OA\Operation(method: 'trace', path: '/pets')] + * ``` + * + * to this: + * + * ```php + * #[OA\Operation\Trace(path: '/pets')] + * ``` + * * @see [Operation Object](https://spec.openapis.org/oas/v3.1.1.html#operation-object) */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] @@ -43,6 +55,7 @@ public function __construct( ?OA\ExternalDocumentation $externalDocs = null, ?array $x = null, ?array $attachables = null, + ?OA\Fan $fan = null, ) { parent::__construct( path: $path, @@ -61,6 +74,7 @@ public function __construct( externalDocs: $externalDocs, x: $x, attachables: $attachables, + fan: $fan, ); } } diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Parameter.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Parameter.php index fde46095..3fd910e2 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/Parameter.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Parameter.php @@ -23,9 +23,9 @@ * new OA\Parameter\Query(name: 'status', schema: new OA\Schema(type: 'string', enum: ['active', 'sold'])), * ])] * - * Or as a reusable component (set `parameter` for the component key): + * Or as a reusable component (set `component` for the component key): * - * #[OA\Parameter\Path(parameter: 'petId', name: 'id', schema: new OA\Schema(type: 'integer'))] + * #[OA\Parameter\Path(component: 'petId', name: 'id', schema: new OA\Schema(type: 'integer'))] * * Produces: * components: @@ -42,6 +42,9 @@ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY | \Attribute::TARGET_PARAMETER | \Attribute::IS_REPEATABLE)] class Parameter extends AbstractAttribute { + /** The key this attribute is filed under in `components`; null for an inline one. */ + public ?string $component = null; + public ?string $in = null; public ?string $style = null; @@ -50,7 +53,7 @@ class Parameter extends AbstractAttribute public ?array $content = null; /** - * @param string|null $parameter Reusable parameter identifier (component key) + * @param string|null $parameter Deprecated since 6.12, removed in 8.0 - use `component` instead * @param string|null $name The name of the parameter * @param string|ParameterIn|null $in The location of the parameter (query, header, path, cookie) * @param string|null $description A brief description of the parameter (CommonMark syntax) @@ -65,8 +68,10 @@ class Parameter extends AbstractAttribute * @param mixed $example Example of the parameter's value * @param list|null $examples Examples of the parameter's value * @param MediaType|list|null $content Content-type based parameter serialization + * @param string|null $component The key this is filed under in `components`, which makes it a reusable component * @param array|null $x Vendor extensions (x-* properties) * @param list|null $attachables Reusable custom attachable attributes + * @param Fan|null $fan Where this attribute goes when more than one attribute could hold it */ public function __construct( public ?string $parameter = null, @@ -84,10 +89,17 @@ public function __construct( public mixed $example = Undefined::UNDEFINED, public ?array $examples = null, MediaType|array|null $content = null, + ?string $component = null, ?array $x = null, ?array $attachables = null, + ?Fan $fan = null, ) { - parent::__construct(x: $x, attachables: $attachables); + parent::__construct(x: $x, attachables: $attachables, fan: $fan); + $this->component = $component; + if ($component === null && $this->parameter !== null) { + trigger_deprecation('zircote/swagger-php', '6.12', '`parameter` is deprecated as the component key of %s and will be removed in 8.0; use `component`', static::class); + $this->component = $this->parameter; + } $this->in = $in instanceof \BackedEnum ? $in->value : $in; $this->style = $style instanceof \BackedEnum ? $style->value : $style; $this->content = self::wrapList($content); @@ -95,7 +107,7 @@ public function __construct( public function isRoot(): bool { - return $this->ref === null && $this->parameter !== null; + return $this->ref === null && $this->component !== null; } public function merge(): array @@ -110,6 +122,7 @@ public function merge(): array public function contained(): array { return [ + Components::class => 'parameters[]', Operation::class => 'parameters[]', PathItem::class => 'parameters[]', ]; diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Parameter/Cookie.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Parameter/Cookie.php index d22a1909..3fd7c95d 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/Parameter/Cookie.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Parameter/Cookie.php @@ -12,6 +12,18 @@ /** * A parameter passed via an HTTP cookie. * + * Allows to shorten this: + * + * ```php + * #[OA\Parameter(name: 'session', in: 'cookie', schema: new OA\Schema(type: 'string'))] + * ``` + * + * to this: + * + * ```php + * #[OA\Parameter\Cookie(name: 'session', schema: new OA\Schema(type: 'string'))] + * ``` + * * @see [Parameter Object](https://spec.openapis.org/oas/v3.1.1.html#parameter-object) */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_PARAMETER | \Attribute::IS_REPEATABLE)] @@ -35,8 +47,10 @@ public function __construct( mixed $example = Undefined::UNDEFINED, ?array $examples = null, OA\MediaType|array|null $content = null, + ?string $component = null, ?array $x = null, ?array $attachables = null, + ?OA\Fan $fan = null, ) { parent::__construct( parameter: $parameter, @@ -51,8 +65,10 @@ public function __construct( example: $example, examples: $examples, content: $content, + component: $component, x: $x, attachables: $attachables, + fan: $fan, ); } } diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Parameter/Header.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Parameter/Header.php index b4cb6ef9..ce3fb1cc 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/Parameter/Header.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Parameter/Header.php @@ -12,6 +12,18 @@ /** * A parameter passed via an HTTP header. * + * Allows to shorten this: + * + * ```php + * #[OA\Parameter(name: 'X-Request-Id', in: 'header', schema: new OA\Schema(type: 'string'))] + * ``` + * + * to this: + * + * ```php + * #[OA\Parameter\Header(name: 'X-Request-Id', schema: new OA\Schema(type: 'string'))] + * ``` + * * @see [Parameter Object](https://spec.openapis.org/oas/v3.1.1.html#parameter-object) */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_PARAMETER | \Attribute::IS_REPEATABLE)] @@ -35,8 +47,10 @@ public function __construct( mixed $example = Undefined::UNDEFINED, ?array $examples = null, OA\MediaType|array|null $content = null, + ?string $component = null, ?array $x = null, ?array $attachables = null, + ?OA\Fan $fan = null, ) { parent::__construct( parameter: $parameter, @@ -51,8 +65,10 @@ public function __construct( example: $example, examples: $examples, content: $content, + component: $component, x: $x, attachables: $attachables, + fan: $fan, ); } } diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Parameter/Path.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Parameter/Path.php index 663d0155..733f44f0 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/Parameter/Path.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Parameter/Path.php @@ -12,6 +12,18 @@ /** * A parameter passed via the URL path (always required). * + * Allows to shorten this: + * + * ```php + * #[OA\Parameter(name: 'id', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))] + * ``` + * + * to this: + * + * ```php + * #[OA\Parameter\Path(name: 'id', schema: new OA\Schema(type: 'integer'))] + * ``` + * * @see [Parameter Object](https://spec.openapis.org/oas/v3.1.1.html#parameter-object) */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_PARAMETER | \Attribute::IS_REPEATABLE)] @@ -36,8 +48,10 @@ public function __construct( mixed $example = Undefined::UNDEFINED, ?array $examples = null, OA\MediaType|array|null $content = null, + ?string $component = null, ?array $x = null, ?array $attachables = null, + ?OA\Fan $fan = null, ) { parent::__construct( parameter: $parameter, @@ -53,8 +67,10 @@ public function __construct( example: $example, examples: $examples, content: $content, + component: $component, x: $x, attachables: $attachables, + fan: $fan, ); } } diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Parameter/Query.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Parameter/Query.php index 3cf79481..cab5617c 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/Parameter/Query.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Parameter/Query.php @@ -12,6 +12,18 @@ /** * A parameter passed via the URL query string. * + * Allows to shorten this: + * + * ```php + * #[OA\Parameter(name: 'limit', in: 'query', schema: new OA\Schema(type: 'integer'))] + * ``` + * + * to this: + * + * ```php + * #[OA\Parameter\Query(name: 'limit', schema: new OA\Schema(type: 'integer'))] + * ``` + * * @see [Parameter Object](https://spec.openapis.org/oas/v3.1.1.html#parameter-object) */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_PARAMETER | \Attribute::IS_REPEATABLE)] @@ -38,8 +50,10 @@ public function __construct( mixed $example = Undefined::UNDEFINED, ?array $examples = null, OA\MediaType|array|null $content = null, + ?string $component = null, ?array $x = null, ?array $attachables = null, + ?OA\Fan $fan = null, ) { parent::__construct( parameter: $parameter, @@ -57,8 +71,10 @@ public function __construct( example: $example, examples: $examples, content: $content, + component: $component, x: $x, attachables: $attachables, + fan: $fan, ); } } diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/PathItem.php b/upload/api/vendor/zircote/swagger-php/src/Spec/PathItem.php index 396b3030..d3a71bf2 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/PathItem.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/PathItem.php @@ -52,6 +52,9 @@ #[\Attribute(\Attribute::TARGET_CLASS)] class PathItem extends AbstractAttribute { + /** The key this attribute is filed under in `components`; null for an inline one. */ + public ?string $component = null; + /** Resolved path — set by augmenter or HybridBridge, not user-authored. */ public ?string $path = null; @@ -65,8 +68,10 @@ class PathItem extends AbstractAttribute * @param list|null $tags Tags to clone to contained operations * @param list|null $security Security requirements to clone to contained operations * @param list|null $responses Shared responses to clone to contained operations + * @param string|null $component The key this is filed under in `components`, which makes it a reusable component * @param array|null $x Vendor extensions (x-* properties) * @param list|null $attachables Reusable custom attachable attributes + * @param Fan|null $fan Where this attribute goes when more than one attribute could hold it */ public function __construct( public string|Schema\Ref|null $ref = null, @@ -78,14 +83,24 @@ public function __construct( public ?array $tags = null, public ?array $security = null, public ?array $responses = null, + ?string $component = null, ?array $x = null, ?array $attachables = null, + ?Fan $fan = null, ) { - parent::__construct(x: $x, attachables: $attachables); + parent::__construct(x: $x, attachables: $attachables, fan: $fan); + $this->component = $component; } public function isRoot(): bool { return true; } + + public function merge(): array + { + return [ + Components::class => 'pathItems[]', + ]; + } } diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Property.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Property.php index 19b9727c..bbe40533 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/Property.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Property.php @@ -23,14 +23,16 @@ class Property extends AbstractAttribute * @param Schema|null $schema The schema defining the property type and constraints * @param array|null $x Vendor extensions (x-* properties) * @param list|null $attachables Reusable custom attachable attributes + * @param Fan|null $fan Where this attribute goes when more than one attribute could hold it */ public function __construct( public ?string $property = null, public ?Schema $schema = null, ?array $x = null, ?array $attachables = null, + ?Fan $fan = null, ) { - parent::__construct(x: $x, attachables: $attachables); + parent::__construct(x: $x, attachables: $attachables, fan: $fan); } public function merge(): array diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Property/Encoded.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Property/Encoded.php index 2c8580ae..ef819ca9 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/Property/Encoded.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Property/Encoded.php @@ -15,6 +15,33 @@ * bundles property and encoding together. The `MediaTypes` augmenter promotes the nested * encoding to the parent MediaType automatically. * + * Allows to shorten this: + * + * ```php + * new OA\MediaType\Json( + * properties: [ + * new OA\Property(property: 'avatar', schema: new OA\Schema(type: 'object')), + * ], + * encoding: [ + * new OA\Encoding(encoding: 'avatar', contentType: 'image/png'), + * ], + * ) + * ``` + * + * to this: + * + * ```php + * new OA\MediaType\Json( + * properties: [ + * new OA\Property\Encoded( + * property: 'avatar', + * schema: new OA\Schema(type: 'object'), + * encoding: new OA\Encoding(contentType: 'image/png'), + * ), + * ], + * ) + * ``` + * * @see [Encoding Object](https://spec.openapis.org/oas/v3.1.1.html#encoding-object) */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::TARGET_PARAMETER | \Attribute::TARGET_CLASS_CONSTANT | \Attribute::IS_REPEATABLE)] @@ -27,6 +54,7 @@ class Encoded extends OA\Property * @param OA\Schema|null $schema The schema defining the property type and constraints * @param array|null $x Vendor extensions (x-* properties) * @param list|null $attachables Reusable custom attachable attributes + * @param OA\Fan|null $fan Where this attribute goes when more than one attribute could hold it */ public function __construct( ?string $property = null, @@ -34,8 +62,9 @@ public function __construct( ?OA\Encoding $encoding = null, ?array $x = null, ?array $attachables = null, + ?OA\Fan $fan = null, ) { - parent::__construct(property: $property, schema: $schema, x: $x, attachables: $attachables); + parent::__construct(property: $property, schema: $schema, x: $x, attachables: $attachables, fan: $fan); $this->encoding = $encoding; } diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/RequestBody.php b/upload/api/vendor/zircote/swagger-php/src/Spec/RequestBody.php index 5f38762a..54bb4776 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/RequestBody.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/RequestBody.php @@ -14,34 +14,46 @@ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY | \Attribute::TARGET_PARAMETER | \Attribute::IS_REPEATABLE)] class RequestBody extends AbstractAttribute { + /** The key this attribute is filed under in `components`; null for an inline one. */ + public ?string $component = null; + /** @var list|null */ public ?array $content = null; /** - * @param string|null $request Reusable request body identifier (component key) - * @param string|null $description A brief description of the request body (CommonMark syntax) - * @param bool|null $required Whether the request body is required - * @param string|Schema\Ref|null $ref A JSON Reference to a reusable request body - * @param MediaType|list|null $content The content of the request body - * @param array|null $x Vendor extensions (x-* properties) - * @param list|null $attachables Reusable custom attachable attributes + * @param string|null $request Deprecated since 6.12, removed in 8.0 - use `component` instead + * @param string|null $description A brief description of the request body (CommonMark syntax) + * @param bool|null $required Whether the request body is required + * @param string|Schema\Ref|null $ref A JSON Reference to a reusable request body + * @param MediaType|list|Schema|null $content The content of the request body; a bare `Schema` is the payload of one media type, which `Augmenter\DefaultMediaType` fills in + * @param string|null $component The key this is filed under in `components`, which makes it a reusable component + * @param array|null $x Vendor extensions (x-* properties) + * @param list|null $attachables Reusable custom attachable attributes + * @param Fan|null $fan Where this attribute goes when more than one attribute could hold it */ public function __construct( public ?string $request = null, public ?string $description = null, public ?bool $required = null, public string|Schema\Ref|null $ref = null, - MediaType|array|null $content = null, + MediaType|Schema|array|null $content = null, + ?string $component = null, ?array $x = null, ?array $attachables = null, + ?Fan $fan = null, ) { - parent::__construct(x: $x, attachables: $attachables); - $this->content = self::wrapList($content); + parent::__construct(x: $x, attachables: $attachables, fan: $fan); + $this->component = $component; + if ($component === null && $this->request !== null) { + trigger_deprecation('zircote/swagger-php', '6.12', '`request` is deprecated as the component key of %s and will be removed in 8.0; use `component`', static::class); + $this->component = $this->request; + } + $this->content = self::wrapContent($content); } public function isRoot(): bool { - return $this->request !== null; + return $this->component !== null; } public function merge(): array @@ -55,6 +67,7 @@ public function merge(): array public function contained(): array { return [ + Components::class => 'requestBodies[]', Operation::class => 'requestBody', ]; } diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Response.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Response.php index 9929cc66..59794f78 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/Response.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Response.php @@ -21,36 +21,44 @@ class Response extends AbstractAttribute */ public const STATUS_CODE_PATTERN = '/^(default|[1-5][0-9]{2}|[1-5]XX)$/'; + /** The key this attribute is filed under in `components`; null for an inline one. */ + public ?string $component = null; + /** @var list|null */ public ?array $content = null; /** - * @param string|int|null $response The HTTP status code or 'default' - * @param string|null $description A description of the response (CommonMark syntax) - * @param string|Schema\Ref|null $ref A JSON Reference to a reusable response - * @param list
|null $headers Headers sent with the response - * @param MediaType|list|null $content Possible response payloads - * @param list|null $links Design-time links for the response - * @param array|null $x Vendor extensions (x-* properties) - * @param list|null $attachables Reusable custom attachable attributes + * @param string|int|null $response The HTTP status code, a range such as '2XX', or 'default' — the key the response nests under in an operation + * @param string|null $description A description of the response (CommonMark syntax) + * @param string|Schema\Ref|null $ref A JSON Reference to a reusable response + * @param list
|null $headers Headers sent with the response + * @param MediaType|list|Schema|null $content Possible response payloads; a bare `Schema` is the payload of one media type, which `Augmenter\DefaultMediaType` fills in + * @param list|null $links Design-time links for the response + * @param string|null $component The key this is filed under in `components`, which makes it a reusable component + * @param array|null $x Vendor extensions (x-* properties) + * @param list|null $attachables Reusable custom attachable attributes + * @param Fan|null $fan Where this attribute goes when more than one attribute could hold it */ public function __construct( public string|int|null $response = null, public ?string $description = null, public string|Schema\Ref|null $ref = null, public ?array $headers = null, - MediaType|array|null $content = null, + MediaType|Schema|array|null $content = null, public ?array $links = null, + ?string $component = null, ?array $x = null, ?array $attachables = null, + ?Fan $fan = null, ) { - parent::__construct(x: $x, attachables: $attachables); - $this->content = self::wrapList($content); + parent::__construct(x: $x, attachables: $attachables, fan: $fan); + $this->component = $component; + $this->content = self::wrapContent($content); } public function isRoot(): bool { - return $this->ref === null && $this->response !== null; + return $this->ref === null && ($this->component !== null || $this->response !== null); } public function merge(): array @@ -65,6 +73,7 @@ public function merge(): array public function contained(): array { return [ + Components::class => 'responses[]', Operation::class => 'responses[]', PathItem::class => 'responses[]', ]; diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Schema.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Schema.php index 76ab1e7c..e347b195 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/Schema.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Schema.php @@ -6,6 +6,7 @@ namespace OpenApi\Spec; +use OpenApi\Spec\Concerns\SchemaInputTrait; use OpenApi\Undefined; /** @@ -38,6 +39,8 @@ * without it the schema has no component key and is reported as missing one. * * @phpstan-type EnumValue string|int|float|bool|\UnitEnum + * @phpstan-type TypeInput string|Type|list + * @phpstan-type PropertiesInput array * * @see [Schema Object](https://spec.openapis.org/oas/v3.1.1.html#schema-object) * @see [JSON Schema](https://json-schema.org/draft/2020-12/json-schema-validation) @@ -45,6 +48,8 @@ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY | \Attribute::TARGET_PARAMETER | \Attribute::IS_REPEATABLE)] class Schema extends AbstractAttribute { + use SchemaInputTrait; + public const ARRAY_PROPERTIES = [ 'items', 'minItems', @@ -57,13 +62,24 @@ class Schema extends AbstractAttribute 'unevaluatedItems', ]; + /** The key this attribute is filed under in `components`; null for an inline one. */ + public ?string $component = null; + + /** @var string|list|null */ + public string|array|null $type = null; + + public ?string $format = null; + + /** @var list|null */ + public ?array $properties = null; + /** - * @param string|null $schema Reusable schema identifier (component key) + * @param string|null $schema Deprecated since 6.12, removed in 8.0 - use `component` instead * @param string|null $title A title for the schema * @param string|null $description A description of the schema (CommonMark syntax) * @param string|null $ref A JSON Reference to a reusable schema - * @param string|list|null $type The value type(s) (string, number, integer, boolean, array, object, null) - * @param string|null $format Further refines the type (e.g. int32, int64, float, double, date-time, email) + * @param TypeInput|null $type The value type(s) (string, number, integer, boolean, array, object, null) + * @param string|Format|null $format Further refines the type (e.g. int32, int64, float, double, date-time, email) * @param bool|null $nullable Whether the value can be null (OAS 3.0 only; use type array in 3.1+) * @param int|null $minLength Minimum string length * @param int|null $maxLength Maximum string length @@ -85,7 +101,7 @@ class Schema extends AbstractAttribute * @param int|null $minContains Minimum number of items matching contains * @param int|null $maxContains Maximum number of items matching contains * @param Schema|bool|null $unevaluatedItems Schema for items not covered by other keywords - * @param list|null $properties Object property definitions + * @param PropertiesInput|null $properties Object property definitions, as a list of `Property` or keyed by property name * @param list|null $required List of required property names * @param Schema|Schema\AdditionalProperties|bool|null $additionalProperties Schema or boolean for additional properties * @param array|null $patternProperties Schemas for properties matching regex patterns @@ -113,8 +129,10 @@ class Schema extends AbstractAttribute * @param Discriminator|null $discriminator Discriminator for polymorphism * @param ExternalDocumentation|null $externalDocs Additional external documentation * @param Xml|null $xml XML representation metadata + * @param string|null $component The key this is filed under in `components`, which makes it a reusable component * @param array|null $x Vendor extensions (x-* properties) * @param list|null $attachables Reusable custom attachable attributes + * @param Fan|null $fan Where this attribute goes when more than one attribute could hold it */ public function __construct( // Identity @@ -126,8 +144,8 @@ public function __construct( public string|Schema\Ref|null $ref = null, // Core type - public string|array|null $type = null, - public ?string $format = null, + string|Type|array|null $type = null, + string|Format|null $format = null, public ?bool $nullable = null, // String constraints @@ -157,7 +175,7 @@ public function __construct( public Schema|bool|null $unevaluatedItems = null, // Object constraints - public ?array $properties = null, + ?array $properties = null, public ?array $required = null, public Schema|Schema\AdditionalProperties|bool|null $additionalProperties = null, public ?array $patternProperties = null, @@ -197,10 +215,20 @@ public function __construct( public ?Discriminator $discriminator = null, public ?ExternalDocumentation $externalDocs = null, public ?Xml $xml = null, + ?string $component = null, ?array $x = null, ?array $attachables = null, + ?Fan $fan = null, ) { - parent::__construct(x: $x, attachables: $attachables); + parent::__construct(x: $x, attachables: $attachables, fan: $fan); + $this->type = self::typeValue($type); + $this->format = $format instanceof Format ? $format->value : $format; + $this->properties = self::propertyList($properties); + $this->component = $component; + if ($component === null && $this->schema !== null) { + trigger_deprecation('zircote/swagger-php', '6.12', '`schema` is deprecated as the component key of %s and will be removed in 8.0; use `component`', static::class); + $this->component = $this->schema; + } } public function isRoot(): bool @@ -218,4 +246,11 @@ public function merge(): array MediaType::class => 'schema', ]; } + + public function contained(): array + { + return [ + Components::class => 'schemas[]', + ]; + } } diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Schema/AdditionalProperties.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Schema/AdditionalProperties.php index 9944df26..d72d225a 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/Schema/AdditionalProperties.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Schema/AdditionalProperties.php @@ -14,10 +14,12 @@ * Identical to OA\Schema in functionality — exists for readability when declaring * schemas with constrained additional properties: * - * new OA\Schema( - * type: 'object', - * additionalProperties: new OA\Schema\AdditionalProperties(type: 'string'), - * ) + * ```php + * new OA\Schema( + * type: 'object', + * additionalProperties: new OA\Schema\AdditionalProperties(type: 'string'), + * ) + * ``` */ final class AdditionalProperties extends OA\Schema { diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Schema/Items.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Schema/Items.php index 3b663333..ae82fb1e 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/Schema/Items.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Schema/Items.php @@ -13,20 +13,24 @@ * * Allows to shorten this: * - * #[OA\Schema] - * class Pet { - * #[OA\Property] - * #[OA\Schema(type: 'array', items: new OA\Schema(ref: MyModel::class))] - * public array $names; - * } + * ```php + * #[OA\Schema] + * class Pet { + * #[OA\Property] + * #[OA\Schema(type: 'array', items: new OA\Schema(ref: MyModel::class))] + * public array $names; + * } + * ``` * * to this: * - * #[OA\Schema] - * class Pet { - * #[OA\Schema\Items(ref: MyModel::class)] - * public array $names; - * } + * ```php + * #[OA\Schema] + * class Pet { + * #[OA\Schema\Items(ref: MyModel::class)] + * public array $names; + * } + * ``` * * Since Items extends Schema, the implicit `OA\Property` shortcut applies — no explicit * `#[OA\Property]` is needed. The `Shortcuts` augmenter wraps this into diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Schema/Ref.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Schema/Ref.php index 4d73d665..fa392c15 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/Schema/Ref.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Schema/Ref.php @@ -15,9 +15,11 @@ * the referenced schema's metadata without duplicating the definition. * * Usage: - * #[OA\Property(schema: new OA\Schema\Ref(ref: Pet::class))] - * #[OA\Property(schema: new OA\Schema\Ref(ref: '#/components/schemas/Pet', title: 'The pet'))] - * #[OA\Property(schema: new OA\Schema\Ref(ref: Pet::class, description: 'Override desc'))] + * ```php + * #[OA\Property(schema: new OA\Schema\Ref(ref: Pet::class))] + * #[OA\Property(schema: new OA\Schema\Ref(ref: '#/components/schemas/Pet', title: 'The pet'))] + * #[OA\Property(schema: new OA\Schema\Ref(ref: Pet::class, description: 'Override desc'))] + * ``` * * If used on a `$ref` directly, only the ref value is used. */ @@ -30,6 +32,7 @@ public function __construct( ?string $description = null, ?array $x = null, ?array $attachables = null, + ?OA\Fan $fan = null, ) { parent::__construct( title: $title, @@ -37,6 +40,7 @@ public function __construct( ref: $ref, x: $x, attachables: $attachables, + fan: $fan, ); } } diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Security/Requirement.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Security/Requirement.php index c69a92a6..bf9ab4cf 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/Security/Requirement.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Security/Requirement.php @@ -7,6 +7,7 @@ namespace OpenApi\Spec\Security; use OpenApi\Spec as OA; +use OpenApi\Utils\SecurityRequirement; /** * A security requirement declaring which security schemes apply. @@ -14,6 +15,21 @@ * Each requirement instance represents one entry in the security array (OR logic). * Multiple schemes within a single requirement represent AND logic. * + * Replaces the raw array form: + * + * ```php + * #[OA\Operation\Get(path: '/pets', security: [['apiKey' => []]])] + * ``` + * + * with: + * + * ```php + * #[OA\Operation\Get(path: '/pets', security: [new OA\Security\Requirement(scheme: 'apiKey')])] + * + * // both schemes at once + * new OA\Security\Requirement(schemes: ['apiKey' => [], 'oauth' => ['read:pets']]) + * ``` + * * @see [Security Requirement Object](https://spec.openapis.org/oas/v3.1.1.html#security-requirement-object) */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] @@ -25,6 +41,7 @@ class Requirement extends OA\AbstractAttribute * @param array>|null $schemes Map of scheme names to scopes (for AND logic with multiple schemes) * @param array|null $x Vendor extensions (x-* properties) * @param list|null $attachables Reusable custom attachable attributes + * @param OA\Fan|null $fan Where this attribute goes when more than one attribute could hold it */ public function __construct( public ?string $scheme = null, @@ -32,8 +49,9 @@ public function __construct( public ?array $schemes = null, ?array $x = null, ?array $attachables = null, + ?OA\Fan $fan = null, ) { - parent::__construct(x: $x, attachables: $attachables); + parent::__construct(x: $x, attachables: $attachables, fan: $fan); } public function merge(): array @@ -49,12 +67,14 @@ public function contained(): array /** * Resolve to the normalised map format: ['schemeName' => [...scopes]]. * + * A bare scheme name in `$schemes` stands for that scheme with no scopes. + * * @return array> */ public function toArray(): array { if ($this->schemes !== null) { - return $this->schemes; + return SecurityRequirement::normalise($this->schemes); } if ($this->scheme !== null) { diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Security/Scheme.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Security/Scheme.php index b6d1e3c7..5c60bb45 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/Security/Scheme.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Security/Scheme.php @@ -21,17 +21,26 @@ * - `OA\Security\Scheme\OpenIdConnect` - OpenID Connect discovery * - `OA\Security\Scheme\MutualTls` - Mutual TLS authentication * + * Example: + * + * ```php + * #[OA\Security\Scheme(component: 'bearerAuth', type: 'http', scheme: 'bearer')] + * ``` + * * @see [Security Scheme Object](https://spec.openapis.org/oas/v3.1.1.html#security-scheme-object) */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)] class Scheme extends OA\AbstractAttribute { + /** The key this attribute is filed under in `components`; null for an inline one. */ + public ?string $component = null; + public ?string $type = null; public ?string $in = null; /** - * @param string|null $securityScheme Reusable security scheme identifier (component key) + * @param string|null $securityScheme Deprecated since 6.12, removed in 8.0 - use `component` instead * @param string|OA\SchemeType|null $type The type of the security scheme (apiKey, http, mutualTLS, oauth2, openIdConnect) * @param string|null $description A description of the security scheme (CommonMark syntax) * @param string|null $name The name of the header, query, or cookie parameter (apiKey) @@ -41,8 +50,10 @@ class Scheme extends OA\AbstractAttribute * @param string|null $openIdConnectUrl The OpenID Connect URL to discover configuration (openIdConnect) * @param list|null $flows The available OAuth2 flows (oauth2) * @param string|null $ref A JSON Reference to a reusable security scheme + * @param string|null $component The key this is filed under in `components`, which makes it a reusable component * @param array|null $x Vendor extensions (x-* properties) * @param list|null $attachables Reusable custom attachable attributes + * @param OA\Fan|null $fan Where this attribute goes when more than one attribute could hold it */ public function __construct( public ?string $securityScheme = null, @@ -55,10 +66,17 @@ public function __construct( public ?string $openIdConnectUrl = null, public ?array $flows = null, public string|OA\Schema\Ref|null $ref = null, + ?string $component = null, ?array $x = null, ?array $attachables = null, + ?OA\Fan $fan = null, ) { - parent::__construct(x: $x, attachables: $attachables); + parent::__construct(x: $x, attachables: $attachables, fan: $fan); + $this->component = $component; + if ($component === null && $this->securityScheme !== null) { + trigger_deprecation('zircote/swagger-php', '6.12', '`securityScheme` is deprecated as the component key of %s and will be removed in 8.0; use `component`', static::class); + $this->component = $this->securityScheme; + } $this->type = $type instanceof \BackedEnum ? $type->value : $type; $this->in = $in instanceof \BackedEnum ? $in->value : $in; } diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Security/Scheme/ApiKey.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Security/Scheme/ApiKey.php index 252b9cfb..614f071f 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/Security/Scheme/ApiKey.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Security/Scheme/ApiKey.php @@ -11,6 +11,27 @@ /** * An API key security scheme (header, query, or cookie). * + * Allows to shorten this: + * + * ```php + * #[OA\Security\Scheme( + * component: 'apiKey', + * type: 'apiKey', + * name: 'X-API-Key', + * in: 'header', + * )] + * ``` + * + * to this: + * + * ```php + * #[OA\Security\Scheme\ApiKey( + * component: 'apiKey', + * name: 'X-API-Key', + * in: 'header', + * )] + * ``` + * * @see [Security Scheme Object](https://spec.openapis.org/oas/v3.1.1.html#security-scheme-object) */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)] @@ -25,8 +46,10 @@ public function __construct( ?string $description = null, ?string $name = null, string|OA\SchemeIn|null $in = null, + ?string $component = null, ?array $x = null, ?array $attachables = null, + ?OA\Fan $fan = null, ) { parent::__construct( securityScheme: $securityScheme, @@ -34,8 +57,10 @@ public function __construct( description: $description, name: $name, in: $in, + component: $component, x: $x, attachables: $attachables, + fan: $fan, ); } } diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Security/Scheme/Http.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Security/Scheme/Http.php index 4f1da53b..ab2a960c 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/Security/Scheme/Http.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Security/Scheme/Http.php @@ -11,6 +11,27 @@ /** * An HTTP authentication security scheme (Basic, Bearer, etc.). * + * Allows to shorten this: + * + * ```php + * #[OA\Security\Scheme( + * component: 'bearerAuth', + * type: 'http', + * scheme: 'bearer', + * bearerFormat: 'JWT', + * )] + * ``` + * + * to this: + * + * ```php + * #[OA\Security\Scheme\Http( + * component: 'bearerAuth', + * scheme: 'bearer', + * bearerFormat: 'JWT', + * )] + * ``` + * * @see [Security Scheme Object](https://spec.openapis.org/oas/v3.1.1.html#security-scheme-object) */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)] @@ -25,8 +46,10 @@ public function __construct( ?string $description = null, ?string $scheme = null, ?string $bearerFormat = null, + ?string $component = null, ?array $x = null, ?array $attachables = null, + ?OA\Fan $fan = null, ) { parent::__construct( securityScheme: $securityScheme, @@ -34,8 +57,10 @@ public function __construct( description: $description, scheme: $scheme, bearerFormat: $bearerFormat, + component: $component, x: $x, attachables: $attachables, + fan: $fan, ); } } diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Security/Scheme/MutualTls.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Security/Scheme/MutualTls.php index 2f4ba2ac..47894c24 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/Security/Scheme/MutualTls.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Security/Scheme/MutualTls.php @@ -11,6 +11,23 @@ /** * A Mutual TLS security scheme. * + * Allows to shorten this: + * + * ```php + * #[OA\Security\Scheme( + * component: 'mtls', + * type: 'mutualTLS', + * )] + * ``` + * + * to this: + * + * ```php + * #[OA\Security\Scheme\MutualTls( + * component: 'mtls', + * )] + * ``` + * * @see [Security Scheme Object](https://spec.openapis.org/oas/v3.1.1.html#security-scheme-object) */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)] @@ -23,15 +40,19 @@ class MutualTls extends OA\Security\Scheme public function __construct( ?string $securityScheme = null, ?string $description = null, + ?string $component = null, ?array $x = null, ?array $attachables = null, + ?OA\Fan $fan = null, ) { parent::__construct( securityScheme: $securityScheme, type: OA\SchemeType::MutualTLS, description: $description, + component: $component, x: $x, attachables: $attachables, + fan: $fan, ); } } diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Security/Scheme/OAuth2.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Security/Scheme/OAuth2.php index 976d96ad..b206d159 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/Security/Scheme/OAuth2.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Security/Scheme/OAuth2.php @@ -11,6 +11,25 @@ /** * An OAuth2 security scheme with one or more flows. * + * Allows to shorten this: + * + * ```php + * #[OA\Security\Scheme( + * component: 'oauth', + * type: 'oauth2', + * flows: [new OA\Flow\ClientCredentials(tokenUrl: 'https://example.com/oauth/token', scopes: [])], + * )] + * ``` + * + * to this: + * + * ```php + * #[OA\Security\Scheme\OAuth2( + * component: 'oauth', + * flows: [new OA\Flow\ClientCredentials(tokenUrl: 'https://example.com/oauth/token', scopes: [])], + * )] + * ``` + * * @see [Security Scheme Object](https://spec.openapis.org/oas/v3.1.1.html#security-scheme-object) */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)] @@ -25,16 +44,20 @@ public function __construct( ?string $securityScheme = null, ?string $description = null, ?array $flows = null, + ?string $component = null, ?array $x = null, ?array $attachables = null, + ?OA\Fan $fan = null, ) { parent::__construct( securityScheme: $securityScheme, type: OA\SchemeType::OAuth2, description: $description, flows: $flows, + component: $component, x: $x, attachables: $attachables, + fan: $fan, ); } } diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Security/Scheme/OpenIdConnect.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Security/Scheme/OpenIdConnect.php index 1787ce72..93960827 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/Security/Scheme/OpenIdConnect.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Security/Scheme/OpenIdConnect.php @@ -11,6 +11,25 @@ /** * An OpenID Connect Discovery security scheme. * + * Allows to shorten this: + * + * ```php + * #[OA\Security\Scheme( + * component: 'oidc', + * type: 'openIdConnect', + * openIdConnectUrl: 'https://example.com/.well-known/openid-configuration', + * )] + * ``` + * + * to this: + * + * ```php + * #[OA\Security\Scheme\OpenIdConnect( + * component: 'oidc', + * openIdConnectUrl: 'https://example.com/.well-known/openid-configuration', + * )] + * ``` + * * @see [Security Scheme Object](https://spec.openapis.org/oas/v3.1.1.html#security-scheme-object) */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)] @@ -24,16 +43,20 @@ public function __construct( ?string $securityScheme = null, ?string $description = null, ?string $openIdConnectUrl = null, + ?string $component = null, ?array $x = null, ?array $attachables = null, + ?OA\Fan $fan = null, ) { parent::__construct( securityScheme: $securityScheme, type: OA\SchemeType::OpenIdConnect, description: $description, openIdConnectUrl: $openIdConnectUrl, + component: $component, x: $x, attachables: $attachables, + fan: $fan, ); } } diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Server.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Server.php index a0c370c5..3df80ad5 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/Server.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Server.php @@ -20,6 +20,7 @@ class Server extends AbstractAttribute * @param list|null $variables Variables for server URL template substitution * @param array|null $x Vendor extensions (x-* properties) * @param list|null $attachables Reusable custom attachable attributes + * @param Fan|null $fan Where this attribute goes when more than one attribute could hold it */ public function __construct( public ?string $url = null, @@ -27,8 +28,9 @@ public function __construct( public ?array $variables = null, ?array $x = null, ?array $attachables = null, + ?Fan $fan = null, ) { - parent::__construct(x: $x, attachables: $attachables); + parent::__construct(x: $x, attachables: $attachables, fan: $fan); } public function isRoot(): bool diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/ServerVariable.php b/upload/api/vendor/zircote/swagger-php/src/Spec/ServerVariable.php index 544dccbb..e3edabd7 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/ServerVariable.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/ServerVariable.php @@ -22,6 +22,7 @@ class ServerVariable extends AbstractAttribute * @param list|null>|null $enum Allowed values for substitution; all cast to string in the specification * @param array|null $x Vendor extensions (x-* properties) * @param list|null $attachables Reusable custom attachable attributes + * @param Fan|null $fan Where this attribute goes when more than one attribute could hold it */ public function __construct( public ?string $serverVariable = null, @@ -30,8 +31,9 @@ public function __construct( public ?array $enum = null, ?array $x = null, ?array $attachables = null, + ?Fan $fan = null, ) { - parent::__construct(x: $x, attachables: $attachables); + parent::__construct(x: $x, attachables: $attachables, fan: $fan); } public function merge(): array diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Tag.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Tag.php index b81e34a4..aee7a693 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Spec/Tag.php +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Tag.php @@ -9,9 +9,12 @@ /** * Adds metadata to a single tag used by the Operation Object. * + * On a method or a class, the tag is also added to the operations declared there: a method's + * own, or every operation of the class. On a class with no operations it only declares the tag. + * * @see [Tag Object](https://spec.openapis.org/oas/v3.1.1.html#tag-object) */ -#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)] +#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class Tag extends AbstractAttribute { /** @@ -23,6 +26,7 @@ class Tag extends AbstractAttribute * @param string|null $kind A machine-readable string to categorize the tag * @param array|null $x Vendor extensions (x-* properties) * @param list|null $attachables Reusable custom attachable attributes + * @param Fan|null $fan Where this attribute goes when more than one attribute could hold it */ public function __construct( public ?string $name = null, @@ -33,8 +37,9 @@ public function __construct( public ?string $kind = null, ?array $x = null, ?array $attachables = null, + ?Fan $fan = null, ) { - parent::__construct(x: $x, attachables: $attachables); + parent::__construct(x: $x, attachables: $attachables, fan: $fan); } public function isRoot(): bool diff --git a/upload/api/vendor/zircote/swagger-php/src/Spec/Type.php b/upload/api/vendor/zircote/swagger-php/src/Spec/Type.php new file mode 100644 index 00000000..565015d7 --- /dev/null +++ b/upload/api/vendor/zircote/swagger-php/src/Spec/Type.php @@ -0,0 +1,24 @@ +|null $x Vendor extensions (x-* properties) * @param list|null $attachables Reusable custom attachable attributes + * @param Fan|null $fan Where this attribute goes when more than one attribute could hold it */ public function __construct( public ?string $name = null, @@ -31,8 +32,9 @@ public function __construct( public ?bool $wrapped = null, ?array $x = null, ?array $attachables = null, + ?Fan $fan = null, ) { - parent::__construct(x: $x, attachables: $attachables); + parent::__construct(x: $x, attachables: $attachables, fan: $fan); } public function merge(): array diff --git a/upload/api/vendor/zircote/swagger-php/src/Specification.php b/upload/api/vendor/zircote/swagger-php/src/Specification.php index 8ce48932..1435a708 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Specification.php +++ b/upload/api/vendor/zircote/swagger-php/src/Specification.php @@ -60,6 +60,9 @@ class Specification /** @var list */ public array $examples = []; + /** @var list reusable media types, OpenAPI 3.2 — only a keyed MediaType is root and lands here */ + public array $mediaTypes = []; + /** @var list */ public array $attachables = []; @@ -87,6 +90,7 @@ public function add(AttributeInterface ...$attributes): static $attribute instanceof OA\Security\Scheme => $this->securitySchemes[] = $attribute, $attribute instanceof OA\Link => $this->links[] = $attribute, $attribute instanceof OA\Example => $this->examples[] = $attribute, + $attribute instanceof OA\MediaType => $this->mediaTypes[] = $attribute, $attribute instanceof OA\Attachable => $this->attachables[] = $attribute, $attribute instanceof OA\Components => $this->addComponentsChildren($attribute), default => throw OpenApiException::fromSource( diff --git a/upload/api/vendor/zircote/swagger-php/src/Specification/ComponentIndex.php b/upload/api/vendor/zircote/swagger-php/src/Specification/ComponentIndex.php index d740e92c..4cf53b0b 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Specification/ComponentIndex.php +++ b/upload/api/vendor/zircote/swagger-php/src/Specification/ComponentIndex.php @@ -100,6 +100,29 @@ public function findHeader(string $ref): ?OA\Header return $result instanceof OA\Header ? $result : null; } + /** + * A parameter's identity, `in:name`, which is how OpenAPI tells two parameters apart. Null + * when either half is unknown. + * + * A parameter that is only a `ref` takes both from the component it points to. One without + * a `name` takes the name of the PHP parameter it was read from, the name + * `Augmenter\Types` gives it later, so the answer is the same before and after that augmenter + * runs. + */ + public function parameterKey(OA\Parameter $parameter): ?string + { + $ref = $parameter->ref instanceof OA\Schema\Ref ? $parameter->ref->ref : $parameter->ref; + $target = is_string($ref) ? $this->findParameter($ref) : null; + + $reflector = $parameter->getReflector(); + $name = $parameter->name + ?? $target->name + ?? ($reflector instanceof \ReflectionParameter ? $reflector->getName() : null); + $in = $parameter->in ?? $target?->in; + + return $name !== null && $in !== null ? $in . ':' . $name : null; + } + /** * @return array FQCN → #/components/{type}/{name} */ diff --git a/upload/api/vendor/zircote/swagger-php/src/Specification/ComponentName.php b/upload/api/vendor/zircote/swagger-php/src/Specification/ComponentName.php index 3dc9bb2c..9ae86790 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Specification/ComponentName.php +++ b/upload/api/vendor/zircote/swagger-php/src/Specification/ComponentName.php @@ -8,6 +8,7 @@ use OpenApi\Contracts\AttributeInterface; use OpenApi\Spec as OA; +use OpenApi\Specification; /** * The key a component is filed under in its bucket. @@ -24,43 +25,118 @@ class ComponentName { /** * Every `components` bucket, named as both the `Specification` property holding it and the - * segment a `$ref` to it carries. + * segment a `$ref` to it carries. `pathItems` doubles as the `paths` source: a `PathItem` + * without a key is path-bound and is not a component. */ - public const BUCKETS = ['schemas', 'responses', 'parameters', 'requestBodies', 'headers', 'securitySchemes', 'links', 'examples']; + public const BUCKETS = ['schemas', 'responses', 'parameters', 'requestBodies', 'headers', 'securitySchemes', 'links', 'examples', 'pathItems', 'mediaTypes']; /** * The key, or null when the component has none and cannot be referenced. + * + * One field on every type, `component`. The historic per-type spellings — `schema`, + * `parameter`, `request`, `securityScheme` — alias onto it in their constructors, and are + * read as a fallback for a value set after construction, which the constructor never sees. The + * ones that are also a nesting key — `response`, `header`, `link`, `example` — cannot: + * the object does not know at construction whether it is nested. This is only ever asked + * of an item in a root bucket, where it is not nested, so here the historic key *is* the + * component key and is read as a fallback. `normalise()` writes the same answer into + * `component` once per build, which is where the spelling is reported as deprecated. */ public static function of(AttributeInterface $item): ?string { - return match (true) { - $item instanceof OA\Schema => $item->schema ?? $item->title, - $item instanceof OA\Response => $item->response !== null ? (string) $item->response : null, - $item instanceof OA\Parameter => $item->parameter ?? $item->name, - $item instanceof OA\RequestBody => $item->request, - $item instanceof OA\Header => $item->header, - $item instanceof OA\Security\Scheme => $item->securityScheme, - $item instanceof OA\Link => $item->link, - $item instanceof OA\Example => $item->example, - default => null, - }; + if (!self::isComponentType($item)) { + return null; + } + + return $item->component ?? self::inferred($item)[1]; } /** * The constructor field a missing key is reported against, for diagnostics. */ public static function keyField(AttributeInterface $item): ?string + { + return self::of($item) === null && !self::isComponentType($item) ? null : 'component'; + } + + /** + * Whether the type can carry a component key at all. + */ + public static function isComponentType(AttributeInterface $item): bool + { + return $item instanceof OA\Schema + || $item instanceof OA\Response + || $item instanceof OA\Parameter + || $item instanceof OA\RequestBody + || $item instanceof OA\Header + || $item instanceof OA\Security\Scheme + || $item instanceof OA\Link + || $item instanceof OA\Example + || $item instanceof OA\PathItem + || $item instanceof OA\MediaType; + } + + /** + * Fill `component` on every root component that still spells its key the historic way. + * + * `response`, `header`, `link` and `example` are the nesting key when the attribute is + * nested and the component key when it is not, and the object cannot tell the two apart + * at construction — nesting is structural, and a nested attribute never reaches a root + * slot. Here it has: anything in a root bucket is a component, so its historic key is the + * component key and aliases across. The one inference that reads a value field — + * `Parameter::$name` — lives here for the same reason. So do the constructor-aliased + * spellings, for a value assigned after construction. + * + * Runs before the resolver builds the first `ComponentIndex`, and again after the hybrid + * bridge adds classic-derived attributes; only the first pass reports the spelling as + * deprecated, since the bridge's callers never wrote it. A `Specification` that never + * passes through here — hand-built and compiled directly — still keys correctly, because + * `of()` reads the same fallback; it only misses the deprecation notice. + */ + public static function normalise(Specification $specification, bool $deprecate): void + { + foreach (self::BUCKETS as $bucket) { + foreach ($specification->{$bucket} as $item) { + if (!self::isComponentType($item) || $item->component !== null) { + continue; + } + + [$field, $value] = self::inferred($item); + if ($value === null) { + continue; + } + + if ($field !== null && $deprecate) { + trigger_deprecation('zircote/swagger-php', '6.12', '`%s` is deprecated as the component key of %s and will be removed in 8.0; use `component`', $field, $item::class); + } + + $item->component = $value; + } + } + } + + /** + * What `component` falls back to: the historic spelling, and the one inference that reads a + * value field — a parameter without one is keyed by its name. The constructors alias the + * spellings that can only be the key, so those are only found here when assigned later. The + * field name is reported when the spelling is deprecated, and is null for the inference, + * which is not. A schema used to fall back to its title; nothing relied on it, and a + * keyless schema is better reported than silently named. + * + * @return array{string|null, string|null} [field, value] + */ + protected static function inferred(AttributeInterface $item): array { return match (true) { - $item instanceof OA\Schema => 'schema', - $item instanceof OA\Response => 'response', - $item instanceof OA\Parameter => 'parameter', - $item instanceof OA\RequestBody => 'request', - $item instanceof OA\Header => 'header', - $item instanceof OA\Security\Scheme => 'securityScheme', - $item instanceof OA\Link => 'link', - $item instanceof OA\Example => 'example', - default => null, + $item instanceof OA\Response => ['response', $item->response === null ? null : (string) $item->response], + $item instanceof OA\Header => ['header', $item->header], + $item instanceof OA\Link => ['link', $item->link], + $item instanceof OA\Example => ['example', $item->example], + $item instanceof OA\Schema => ['schema', $item->schema], + $item instanceof OA\RequestBody => ['request', $item->request], + $item instanceof OA\Security\Scheme => ['securityScheme', $item->securityScheme], + $item instanceof OA\Parameter => $item->parameter !== null ? ['parameter', $item->parameter] : [null, $item->name], + default => [null, null], }; } } diff --git a/upload/api/vendor/zircote/swagger-php/src/Specification/PathItemHierarchy.php b/upload/api/vendor/zircote/swagger-php/src/Specification/PathItemHierarchy.php index b886bb18..36b3bb0d 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Specification/PathItemHierarchy.php +++ b/upload/api/vendor/zircote/swagger-php/src/Specification/PathItemHierarchy.php @@ -33,10 +33,16 @@ class PathItemHierarchy /** @var array> */ protected array $chains = []; + protected ?ComponentIndex $componentIndex = null; + public function __construct( protected Specification $specification, ) { foreach ($specification->pathItems as $pathItem) { + if ($pathItem->component !== null) { + continue; // a reusable path item, filed under components; it governs no class + } + $className = $pathItem->getClassName(); if ($className !== null) { $this->classToPathItem[$className] = $pathItem; @@ -125,4 +131,76 @@ public function forOperation(OA\Operation $operation): array return $reflector instanceof \ReflectionClass ? $this->chainFor($reflector) : []; } + + /** + * The path prefix the chain governing an operation's class composes to, `''` for none. + * + * Each prefix is trimmed of slashes and the rest are joined outermost first, so `/api/v1` + * and `/users/` compose to `/api/v1/users`. + */ + public function prefixFor(OA\Operation $operation): string + { + $parts = []; + foreach ($this->forOperation($operation) as $pathItem) { + if ($pathItem->prefix !== null && ($part = trim($pathItem->prefix, '/')) !== '') { + $parts[] = $part; + } + } + + return $parts !== [] ? '/' . implode('/', $parts) : ''; + } + + /** + * The path an operation compiles to, its own path with `prefixFor()` in front, or null when + * it has no path. + * + * It reads the path as declared. `Augmenter\PathItems` writes this value back to the + * operation, so after that augmenter has run the prefix is already there and asking again + * adds it twice. + */ + public function pathFor(OA\Operation $operation): ?string + { + if ($operation->path === null) { + return null; + } + + $prefix = $this->prefixFor($operation); + if ($prefix === '') { + return $operation->path; + } + + $path = ltrim($operation->path, '/'); + + return $path !== '' ? $prefix . '/' . $path : $prefix; + } + + /** + * The parameters the chain governing an operation's class declares, keyed `in:name`. + * + * They are emitted at path level and apply to every operation under the path. Where two + * links of the chain declare the same parameter, the nearer one wins, as a subclass + * overrides its parent. The key is `ComponentIndex::parameterKey()`, so a parameter that is + * only a `ref` is keyed by its component's name and location. One with no identity at all is + * left out, since nothing could match it. + * + * The operation's own parameters are not included. + * + * @return array + */ + public function parametersFor(OA\Operation $operation): array + { + $index = $this->componentIndex ??= $this->specification->buildComponentIndex(); + + $parameters = []; + foreach ($this->forOperation($operation) as $pathItem) { + foreach ($pathItem->parameters ?? [] as $parameter) { + $key = $index->parameterKey($parameter); + if ($key !== null) { + $parameters[$key] = $parameter; + } + } + } + + return $parameters; + } } diff --git a/upload/api/vendor/zircote/swagger-php/src/Type/LegacyTypeResolver.php b/upload/api/vendor/zircote/swagger-php/src/Type/LegacyTypeResolver.php index a66d09c2..3fcec555 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Type/LegacyTypeResolver.php +++ b/upload/api/vendor/zircote/swagger-php/src/Type/LegacyTypeResolver.php @@ -42,8 +42,13 @@ protected function doAugment(Analysis $analysis, OA\Schema $schema, \Reflector $ if ('int' === $schema->type && is_array($details->explicitDetails)) { if (array_key_exists('min', $details->explicitDetails)) { - $schema->minimum = $details->explicitDetails['min']; - $schema->maximum = $details->explicitDetails['max']; + // an open end is `PHP_INT_MIN`/`PHP_INT_MAX`, which is no bound of the API + if (\PHP_INT_MIN !== $details->explicitDetails['min']) { + $schema->minimum = $details->explicitDetails['min']; + } + if (\PHP_INT_MAX !== $details->explicitDetails['max']) { + $schema->maximum = $details->explicitDetails['max']; + } } elseif ('non-zero-int' === $details->explicitType) { $schema->not = ['const' => 0]; } @@ -99,8 +104,8 @@ protected function augmentItems(OA\Schema $schema, Analysis $analysis): void } } - if (!Undefined::isDefault($schema->items)) { - $this->mapNativeType($schema->items, $schema->items->type); + if ($schema->items instanceof OA\Items && !Undefined::isDefault($schema->items->type) && !$this->mapNativeType($schema->items, $schema->items->type)) { + $schema->items->type = Undefined::UNDEFINED; } $schema->type = 'array'; } diff --git a/upload/api/vendor/zircote/swagger-php/src/Type/TypeInfoTypeResolver.php b/upload/api/vendor/zircote/swagger-php/src/Type/TypeInfoTypeResolver.php index 684e705f..046444a3 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Type/TypeInfoTypeResolver.php +++ b/upload/api/vendor/zircote/swagger-php/src/Type/TypeInfoTypeResolver.php @@ -10,6 +10,7 @@ use OpenApi\Annotations as OA; use OpenApi\Context; use OpenApi\Undefined; +use Psr\Log\LoggerInterface; class TypeInfoTypeResolver extends AbstractTypeResolver { @@ -28,6 +29,9 @@ public function __construct() */ protected function doAugment(Analysis $analysis, OA\Schema $schema, \Reflector $reflector, string $sourceClass = OA\Schema::class): void { + if ($analysis->context?->logger instanceof LoggerInterface) { + $this->resolver->setLogger($analysis->context->logger); + } $schemaType = $this->resolver->resolve($reflector); if (!$schemaType instanceof SchemaType) { @@ -71,7 +75,8 @@ protected function handlePostAugment(OA\Schema $schema): void */ protected function applyToAnnotation(OA\Schema $schema, SchemaType $schemaType, Analysis $analysis, string $sourceClass = OA\Schema::class): void { - if ($schemaType->type !== null) { + // a class is referenced only when there is no explicit enum it would replace + if ($schemaType->type !== null && (!$schemaType->isRef() || Undefined::isDefault($schema->enum))) { /** @var non-empty-array|string $type every SchemaType array type is built non-empty */ $type = $schemaType->type; $schema->type = $type; @@ -81,15 +86,15 @@ protected function applyToAnnotation(OA\Schema $schema, SchemaType $schemaType, $schema->format = $schemaType->format; } - if ($schemaType->minimum !== null) { + if ($schemaType->minimum !== null && Undefined::isDefault($schema->minimum)) { $schema->minimum = $schemaType->minimum; } - if ($schemaType->maximum !== null) { + if ($schemaType->maximum !== null && Undefined::isDefault($schema->maximum)) { $schema->maximum = $schemaType->maximum; } - if ($schemaType->not !== null) { + if ($schemaType->not !== null && Undefined::isDefault($schema->not)) { $schema->not = $schemaType->not; } @@ -104,7 +109,7 @@ protected function applyToAnnotation(OA\Schema $schema, SchemaType $schemaType, $this->applyToAnnotation($schema->items, $schemaType->items, $analysis, $sourceClass); $this->type2ref($schema->items, $analysis, $sourceClass); } - $this->mapNativeType($schema->items, $schema->items->type); + $this->handlePostAugment($schema->items); } if ($schemaType->additionalProperties instanceof SchemaType) { @@ -123,7 +128,7 @@ protected function applyToAnnotation(OA\Schema $schema, SchemaType $schemaType, $this->applyToAnnotation($schema->additionalProperties, $schemaType->additionalProperties, $analysis, $sourceClass); $this->type2ref($schema->additionalProperties, $analysis, $sourceClass); } - $this->mapNativeType($schema->additionalProperties, $schema->additionalProperties->type); + $this->handlePostAugment($schema->additionalProperties); } } elseif ($schemaType->additionalProperties === true) { if (Undefined::isDefault($schema->additionalProperties)) { @@ -137,11 +142,18 @@ protected function applyToAnnotation(OA\Schema $schema, SchemaType $schemaType, return; } $schema->type = Undefined::UNDEFINED; + // a member typed with a class that is no component matches anything, and so does the union + foreach ($schemaType->oneOf as $childType) { + if ($childType->isRef() && is_string($childType->type) && !$analysis->getAnnotationForSource($childType->type, $sourceClass) instanceof OA\AbstractAnnotation) { + return; + } + } $schema->oneOf = []; foreach ($schemaType->oneOf as $childType) { $childSchema = new OA\Schema(['_context' => new Context(['generated' => true], $schema->_context)]); $this->applyToAnnotation($childSchema, $childType, $analysis, $sourceClass); $this->type2ref($childSchema, $analysis, $sourceClass); + $this->handlePostAugment($childSchema); $analysis->addAnnotation($childSchema, $childSchema->_context); $schema->oneOf[] = $childSchema; } @@ -154,6 +166,7 @@ protected function applyToAnnotation(OA\Schema $schema, SchemaType $schemaType, $childSchema = new OA\Schema(['_context' => new Context(['generated' => true], $schema->_context)]); $this->applyToAnnotation($childSchema, $childType, $analysis, $sourceClass); $this->type2ref($childSchema, $analysis, $sourceClass); + $this->handlePostAugment($childSchema); $analysis->addAnnotation($childSchema, $childSchema->_context); $schema->allOf[] = $childSchema; } @@ -169,7 +182,7 @@ protected function applyToAnnotation(OA\Schema $schema, SchemaType $schemaType, ]); $this->applyToAnnotation($property, $propType, $analysis, $sourceClass); $this->type2ref($property, $analysis, $sourceClass); - $this->mapNativeType($property, $property->type); + $this->handlePostAugment($property); $analysis->addAnnotation($property, $property->_context); $properties[] = $property; } diff --git a/upload/api/vendor/zircote/swagger-php/src/Type/TypeMapper.php b/upload/api/vendor/zircote/swagger-php/src/Type/TypeMapper.php index e4a00890..af5ac837 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Type/TypeMapper.php +++ b/upload/api/vendor/zircote/swagger-php/src/Type/TypeMapper.php @@ -17,7 +17,7 @@ class TypeMapper 'bool' => 'boolean', 'int' => 'integer', 'integer' => 'integer', - 'long' => ['integer', 'long'], + 'long' => ['integer', 'int64'], 'float' => ['number', 'float'], 'double' => ['number', 'double'], 'date' => ['string', 'date'], @@ -31,6 +31,31 @@ class TypeMapper 'object' => 'object', ]; + /** + * The OpenAPI type of a PHP value, or `null` where the value has none. + * + * An array is an `array` where it is a list, an empty one included, and an `object` otherwise. + */ + public static function valueType(mixed $value): ?string + { + return match (true) { + is_string($value) => 'string', + is_int($value) => 'integer', + is_float($value) => 'number', + is_bool($value) => 'boolean', + is_array($value) => array_is_list($value) ? 'array' : 'object', + default => null, + }; + } + + /** + * The value an enum case stands for in a document: a backed case's value, a unit case's name. + */ + public static function caseValue(\UnitEnum $case): int|string + { + return $case instanceof \BackedEnum ? $case->value : $case->name; + } + /** * Map a native PHP type to its OpenAPI type and optional format. * diff --git a/upload/api/vendor/zircote/swagger-php/src/Type/TypeResolver.php b/upload/api/vendor/zircote/swagger-php/src/Type/TypeResolver.php index 7d0c50f9..ea69b883 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Type/TypeResolver.php +++ b/upload/api/vendor/zircote/swagger-php/src/Type/TypeResolver.php @@ -6,15 +6,22 @@ namespace OpenApi\Type; +use PHPStan\PhpDocParser\Ast\AbstractNodeVisitor; +use PHPStan\PhpDocParser\Ast\Node; +use PHPStan\PhpDocParser\Ast\NodeTraverser; use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\PhpDocParser\Lexer\Lexer; use PHPStan\PhpDocParser\Parser\ConstExprParser; use PHPStan\PhpDocParser\Parser\PhpDocParser; use PHPStan\PhpDocParser\Parser\TokenIterator; use PHPStan\PhpDocParser\Parser\TypeParser; use PHPStan\PhpDocParser\ParserConfig; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerAwareTrait; use Radebatz\TypeInfoExtras\Type\ExplicitType; use Radebatz\TypeInfoExtras\Type\IntRangeType; use Radebatz\TypeInfoExtras\TypeResolver\StringTypeResolver; @@ -29,9 +36,11 @@ use Symfony\Component\TypeInfo\Type\NullableType; use Symfony\Component\TypeInfo\Type\ObjectType; use Symfony\Component\TypeInfo\Type\UnionType; +use Symfony\Component\TypeInfo\TypeContext\TypeContext; use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory; use Symfony\Component\TypeInfo\TypeIdentifier; use Symfony\Component\TypeInfo\TypeResolver\ReflectionTypeResolver; +use Symfony\Component\TypeInfo\TypeResolver\StringTypeResolver as SymfonyStringTypeResolver; /** * Resolves a PHP reflector to a SchemaType value object. @@ -39,8 +48,25 @@ * Shared core between the spec-attributes augmenter and the classic annotation resolver. * Uses Symfony TypeInfo + phpstan/phpdoc-parser for reflective type resolution. */ -class TypeResolver +class TypeResolver implements LoggerAwareInterface { + use LoggerAwareTrait; + + /** + * Docblock keywords PHP reserves. No class can have one of these names, in any letter case. + */ + protected const RESERVED_KEYWORDS = [ + 'array', 'bool', 'callable', 'false', 'float', 'int', 'iterable', 'mixed', 'never', 'null', + 'object', 'parent', 'self', 'static', 'string', 'true', 'void', + ]; + + /** + * Docblock keywords that are also valid class names. Each is a keyword only where no class of that name exists. + */ + protected const ALIAS_KEYWORDS = [ + 'boolean', 'double', 'integer', 'list', 'number', 'numeric', 'resource', 'scalar', + ]; + protected TypeMapper $typeMapper; public function __construct() @@ -99,7 +125,7 @@ public function getDocblockType(\Reflector $reflector): ?Type return null; } - $typeContext = (new TypeContextFactory())->createFromReflection($reflector); + $typeContext = (new TypeContextFactory(new SymfonyStringTypeResolver()))->createFromReflection($reflector); $tagName = match (true) { $reflector instanceof \ReflectionProperty => $reflector->isPromoted() @@ -134,10 +160,12 @@ public function getDocblockType(\Reflector $reflector): ?Type || ($tagValue instanceof ParamTagValueNode && '$' . $reflector->getName() === $tagValue->parameterName) || $tagValue instanceof ReturnTagValueNode ) { + $this->lowercaseKeywords($tagValue->type, $typeContext); + try { return (new StringTypeResolver())->resolve((string) $tagValue, $typeContext); } catch (UnsupportedException) { - // ignore + $this->logger?->warning(sprintf('Unsupported docblock type "%s" on %s', $tagValue->type, $this->describe($reflector))); } } } @@ -145,6 +173,52 @@ public function getDocblockType(\Reflector $reflector): ?Type return null; } + /** + * Name a member for a log message: `Pet::$name`, `Pet::name()`, `$id of Pet::get()`. + */ + public function describe(\Reflector $reflector): string + { + return match (true) { + $reflector instanceof \ReflectionProperty => $reflector->class . '::$' . $reflector->name, + $reflector instanceof \ReflectionParameter => '$' . $reflector->name . ' of ' . $this->describe($reflector->getDeclaringFunction()), + $reflector instanceof \ReflectionMethod => $reflector->class . '::' . $reflector->name . '()', + $reflector instanceof \ReflectionFunctionAbstract => $reflector->name . '()', + default => $reflector::class, + }; + } + + /** + * PHPDoc does not enforce case on keywords, so `@var String` means `string`. + */ + protected function lowercaseKeywords(TypeNode $type, TypeContext $typeContext): void + { + $keyword = static function (string $name) use ($typeContext): ?string { + $lowercase = strtolower($name); + + return in_array($lowercase, self::RESERVED_KEYWORDS, true) + || (in_array($lowercase, self::ALIAS_KEYWORDS, true) && !class_exists($typeContext->normalize($name))) + ? $lowercase + : null; + }; + + $visitor = new class ($keyword) extends AbstractNodeVisitor { + public function __construct(protected \Closure $keyword) + { + } + + public function enterNode(Node $node): ?Node + { + if ($node instanceof IdentifierTypeNode) { + $node->name = ($this->keyword)($node->name) ?? $node->name; + } + + return null; + } + }; + + (new NodeTraverser([$visitor]))->traverse([$type]); + } + protected function mapType(Type $type): SchemaType { if ($type instanceof CompositeTypeInterface) { @@ -165,14 +239,20 @@ protected function mapType(Type $type): SchemaType return new SchemaType(type: 'object'); } + // the date classes themselves are mapped by name, a subclass of them by what it is + if (is_a($className, \DateTimeInterface::class, true)) { + return new SchemaType(type: 'string', format: 'date-time'); + } + return new SchemaType(type: $className); } if ($type instanceof IntRangeType) { + // an open end comes as `PHP_INT_MIN`/`PHP_INT_MAX`, which is no bound of the API return new SchemaType( type: $type->getTypeIdentifier()->value, - minimum: $type->getFrom(), - maximum: $type->getTo(), + minimum: \PHP_INT_MIN === $type->getFrom() ? null : $type->getFrom(), + maximum: \PHP_INT_MAX === $type->getTo() ? null : $type->getTo(), ); } diff --git a/upload/api/vendor/zircote/swagger-php/src/Utils/AttributeFactory.php b/upload/api/vendor/zircote/swagger-php/src/Utils/AttributeFactory.php index d5796194..d589f9d4 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Utils/AttributeFactory.php +++ b/upload/api/vendor/zircote/swagger-php/src/Utils/AttributeFactory.php @@ -7,10 +7,16 @@ namespace OpenApi\Utils; use OpenApi\Assembler\DefaultAttributeTranslator; +use OpenApi\Assembler\OperationMethodsTranslator; use OpenApi\Assembler\OptionalPropertyAttributeTranslator; use OpenApi\Contracts\AttributeInterface; use OpenApi\Contracts\AttributeTranslatorInterface; use OpenApi\OpenApiException; +use OpenApi\Spec as OA; +use OpenApi\Specification\ComponentName; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerAwareTrait; +use Psr\Log\LoggerInterface; /** * Creates spec attribute instances from PHP reflectors. @@ -20,8 +26,10 @@ * collection) and augmenter pipes that need to manufacture spec objects * from reflection (e.g. Augmenter\Inheritance for non-schema ancestor members). */ -class AttributeFactory +class AttributeFactory implements LoggerAwareInterface { + use LoggerAwareTrait; + /** * @var TypedList */ @@ -33,6 +41,7 @@ public function __construct(protected TokenScanner $tokenScanner = new TokenScan $translators = [ new DefaultAttributeTranslator(), new OptionalPropertyAttributeTranslator(), + new OperationMethodsTranslator(), ]; $this->translators = new TypedList($translators); @@ -72,13 +81,17 @@ public function withTranslators(callable $hook): static } /** - * Reset all translators. + * Reset all translators, and give those that log this factory's logger. * * Called at the start of each top-level collection unit. */ public function resetTranslators(): void { foreach ($this->translators as $translator) { + if ($translator instanceof LoggerAwareInterface && $this->logger instanceof LoggerInterface) { + $translator->setLogger($this->logger); + } + $translator->reset(); } } @@ -143,14 +156,16 @@ public function membersOf(\ReflectionClass $class): array continue; } - array_push($inner, ...$this->resolveNesting($this->readAttributes($property))); + array_push($inner, ...$this->fromMember($property)); } $constructor = $class->getConstructor(); if ($constructor) { foreach ($constructor->getParameters() as $parameter) { - array_push($inner, ...$this->resolveNesting($this->readAttributes($parameter))); + array_push($inner, ...$this->fromMember($parameter)); } + + $this->reportConstructorAttributes($class, $constructor); } foreach ($class->getReflectionConstants() as $constant) { @@ -160,16 +175,40 @@ public function membersOf(\ReflectionClass $class): array continue; } - array_push($inner, ...$this->resolveNesting($this->readAttributes($constant))); + array_push($inner, ...$this->fromMember($constant)); } foreach ($this->getDirectMethods($class) as $method) { - array_push($inner, ...$this->fromReflector($method)); + array_push($inner, ...$this->fromMember($method)); } return $inner; } + /** + * Read and resolve the attributes of one class member, as {@see self::fromReflector()} does. + * + * A member that fails assembly, such as an attribute several attributes could hold, is left + * out with a warning, so the rest of its class is kept. Without a logger there is nobody to + * tell, so the error is thrown as {@see self::fromReflector()} throws it. + * + * @return list + */ + public function fromMember(\ReflectionMethod|\ReflectionProperty|\ReflectionParameter|\ReflectionClassConstant $member): array + { + try { + return $this->fromReflector($member); + } catch (OpenApiException $openApiException) { + if (!$this->logger instanceof LoggerInterface) { + throw $openApiException; + } + + $this->logger->warning(sprintf('Skipping %s: %s', $this->memberLabel($member), $openApiException->getMessage())); + + return []; + } + } + /** * Check whether a reflector has any `AttributeInterface` attributes. * @@ -261,8 +300,10 @@ public function getDirectTraits(\ReflectionClass $class): array /** * Hierarchical absorb: inner-level attributes declare which outer-level types can contain them. * - * Inner attributes that match an outer attribute via contained() are nested into it (first match wins). - * Inner attributes that find no container pass through alongside outer. + * An inner attribute matching one outer attribute via contained() is nested into it. Matching + * more than one is an error, unless the inner attribute fans out to all of them. Inner + * attributes that find no container pass through alongside outer, and are no container for + * the inner attributes after them: one member's attribute does not nest into another's. * * @param list $outer * @param list $inner @@ -270,28 +311,113 @@ public function getDirectTraits(\ReflectionClass $class): array */ public function resolveHierarchy(array $outer, array $inner): array { + // kept apart from $outer while iterating, so a passing attribute is no container for the next ones + $passing = []; foreach ($inner as $innerAttribute) { - $absorbed = false; - $containedTargets = $innerAttribute->contained(); - - if ($containedTargets !== []) { - foreach ($outer as $outerAttribute) { - foreach ($containedTargets as $parentClass => $slot) { - if ($outerAttribute instanceof $parentClass) { - $this->nestChild($outerAttribute, $innerAttribute, $slot); - $absorbed = true; - break 2; - } - } + $matches = []; + foreach ($outer as $outerAttribute) { + $match = $this->matchTarget($innerAttribute, $outerAttribute, $innerAttribute->contained()); + if ($match !== null) { + $matches[] = $match; } } - if (!$absorbed) { - $outer[] = $innerAttribute; + if ($matches === []) { + $passing[] = $innerAttribute; + continue; } + + $this->nestIntoMatches($innerAttribute, $matches, 'Ambiguous hierarchy: %s matches multiple containers'); } - return $outer; + return [...$outer, ...$passing]; + } + + /** + * A component declared on a member is one only in a class carrying `OA\Components`. Without + * it, the key alone does not say which is meant: on a property the same schema is the + * property's own, and on a method it would be a component. So a keyed component left over + * from a member is dropped, and said so. + * + * A schema on a property arrives inside the `OA\Property` the shortcut adds, so its key is + * looked for there. Unkeyed attributes are left alone: a parent class without a schema leaves + * its properties unabsorbed on purpose, for `Augmenter\Inheritance` to read again. + * + * @param list $outer the class's own attributes + * @param list $resolved the class's attributes after absorbing its members + * + * @return list + */ + public function withoutStrayComponents(array $outer, array $resolved): array + { + foreach ($outer as $attribute) { + if ($attribute instanceof OA\Components) { + return $resolved; + } + } + + $kept = []; + foreach ($resolved as $attribute) { + $component = $attribute instanceof OA\Property ? $attribute->schema : $attribute; + $key = $component instanceof AttributeInterface && ($component->isRoot() || $attribute instanceof OA\Property) + ? ComponentName::of($component) + : null; + + if ($key === null || in_array($attribute, $outer, true)) { + $kept[] = $attribute; + continue; + } + + $this->logger?->warning(sprintf( + '%s "%s" ignored, a component declared on a member needs %s on its class, in %s', + $component::class, + $key, + OA\Components::class, + $attribute->getSourceLocation(), + )); + } + + return $kept; + } + + protected function memberLabel(\ReflectionMethod|\ReflectionProperty|\ReflectionParameter|\ReflectionClassConstant $member): string + { + return match (true) { + $member instanceof \ReflectionMethod => $member->class . '::' . $member->name . '()', + $member instanceof \ReflectionProperty => $member->class . '::$' . $member->name, + $member instanceof \ReflectionClassConstant => $member->class . '::' . $member->name, + default => '$' . $member->name . ' of ' . $member->getDeclaringClass()?->getName() . '::' . $member->getDeclaringFunction()->name . '()', + }; + } + + /** + * The constructor is not a place to declare anything: only its parameters, as promoted + * properties, are read. What sits on it is ignored, and said so; the class takes the same + * attributes. + * + * @param \ReflectionClass $class + */ + protected function reportConstructorAttributes(\ReflectionClass $class, \ReflectionMethod $constructor): void + { + // inherited or brought in by a trait, it is reported where it is declared + $scannerDetails = $this->tokenScanner->detailsFor($class); + if ($constructor->getDeclaringClass()->getName() !== $class->getName() + || ($scannerDetails && !in_array($constructor->getName(), $scannerDetails['methods'], true)) + ) { + return; + } + + $attributes = $constructor->getAttributes(AttributeInterface::class, \ReflectionAttribute::IS_INSTANCEOF); + if ($attributes === []) { + return; + } + + $this->logger?->warning(sprintf( + '%s ignored on a constructor, declare %s on the class in %s', + implode(', ', array_map(static fn (\ReflectionAttribute $attribute): string => $attribute->getName(), $attributes)), + count($attributes) === 1 ? 'it' : 'them', + SourceLocation::fromReflector($constructor), + )); } /** @@ -375,7 +501,8 @@ protected function isMergeTarget(array $attributes, array $pending, int $index): } /** - * Merge one attribute into a matching unmerged sibling, if there is exactly one. + * Merge one attribute into its matching unmerged sibling. Matching more than one is an error, + * unless the attribute fans out to all of them. * * @param list $attributes * @param array $merged @@ -383,35 +510,104 @@ protected function isMergeTarget(array $attributes, array $pending, int $index): protected function mergeIntoSibling(array $attributes, int $index, array &$merged): void { $attribute = $attributes[$index]; - $mergeTargets = $attribute->merge(); - $matchingTarget = null; - $matchingSlot = null; + $matches = []; foreach ($attributes as $candidateIndex => $candidate) { if ($candidateIndex === $index || isset($merged[$candidateIndex])) { continue; } - foreach ($mergeTargets as $targetClass => $slot) { - if ($candidate instanceof $targetClass) { - if ($matchingTarget instanceof AttributeInterface) { - throw OpenApiException::fromSource( - sprintf('Ambiguous merge: %s matches multiple siblings on the same target', $attribute::class), - $attribute->getSourceLocation(), - ); - } - $matchingTarget = $candidate; - $matchingSlot = $slot; - } + $match = $this->matchTarget($attribute, $candidate, $attribute->merge()); + if ($match !== null) { + $matches[] = $match; } } - if ($matchingTarget instanceof AttributeInterface) { - $this->nestChild($matchingTarget, $attribute, $matchingSlot); + if ($matches !== []) { + $this->nestIntoMatches($attribute, $matches, 'Ambiguous merge: %s matches multiple siblings on the same target'); $merged[$index] = true; } } + /** + * Nest a child into the one parent it matched, or with `Fan::All` into each of several that + * match by the same target type: the child itself into the first, a copy into every other. + * Several matches are an error otherwise, and so are matches of different target types, + * whatever the child's `fan`. + * + * @param non-empty-list}> $matches parent, slot and the target types it matched by + */ + protected function nestIntoMatches(AttributeInterface $child, array $matches, string $ambiguous): void + { + if (count($matches) > 1) { + // one type when a target type is shared by every match, whichever order they come in + $shared = array_intersect(...array_column($matches, 2)); + $fanned = $child->getFan() === OA\Fan::All; + + if (!$fanned || $shared === []) { + throw OpenApiException::fromSource( + sprintf( + '%s (%s). %s', + sprintf($ambiguous, $child::class), + implode(', ', array_map($this->matchLabel(...), array_column($matches, 0))), + $fanned + ? 'Fan::All only puts it into several attributes of one type. Nest it explicitly in the one it belongs to.' + : 'Nest it explicitly in the one it belongs to, give it a `component` and reference it from each, or set `fan: Fan::All` to put it in all of them.', + ), + $child->getSourceLocation(), + ); + } + } + + foreach ($matches as $position => [$parent, $slot]) { + $this->nestChild($parent, $position === 0 ? $child : $child->copy(), $slot); + } + } + + /** + * The slot a child takes in a candidate parent, with the target types of `$targets` the + * candidate is an instance of, or null when it is none of them. A candidate matching + * several target types that name different slots is an error: the slot would be decided by + * the order of `$targets`. + * + * @param array $targets target type and slot, from `merge()` or `contained()` + * + * @return array{AttributeInterface, string, list}|null + */ + protected function matchTarget(AttributeInterface $child, AttributeInterface $candidate, array $targets): ?array + { + $matched = array_filter($targets, static fn (string $slot, string $type): bool => $candidate instanceof $type, ARRAY_FILTER_USE_BOTH); + if ($matched === []) { + return null; + } + + $slots = array_values(array_unique($matched)); + if (count($slots) > 1) { + throw OpenApiException::fromSource( + sprintf( + 'Ambiguous slot: %s matches %s as more than one target type (%s), and they name different slots (%s)', + $child::class, + $this->matchLabel($candidate), + implode(', ', array_keys($matched)), + implode(', ', $slots), + ), + $child->getSourceLocation(), + ); + } + + return [$candidate, $slots[0], array_keys($matched)]; + } + + /** + * An attribute as an ambiguity message names it: its class, and its component key if it has one. + */ + protected function matchLabel(AttributeInterface $attribute): string + { + $component = ComponentName::isComponentType($attribute) ? ComponentName::of($attribute) : null; + + return $component !== null ? sprintf('%s "%s"', $attribute::class, $component) : $attribute::class; + } + protected function nestChild(AttributeInterface $parent, AttributeInterface $child, string $slot): void { $validateSlot = function (AttributeInterface $parent, AttributeInterface $child, string $slot): void { diff --git a/upload/api/vendor/zircote/swagger-php/src/Utils/Pipeline.php b/upload/api/vendor/zircote/swagger-php/src/Utils/Pipeline.php index cecc2809..eadbf999 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Utils/Pipeline.php +++ b/upload/api/vendor/zircote/swagger-php/src/Utils/Pipeline.php @@ -8,6 +8,7 @@ use OpenApi\OpenApiException; use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerAwareTrait; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; @@ -16,8 +17,15 @@ * * @extends TypedList */ -class Pipeline extends TypedList +class Pipeline extends TypedList implements LoggerAwareInterface { + /** + * The pipeline hands this to every pipe that asks for one, so setting it here reaches all + * of them. The builder sets it once the build's collecting logger exists, which is after + * the pipeline was built. + */ + use LoggerAwareTrait; + /** * @var list|null ordered group keys; null means no grouping (insertion order only) */ @@ -25,8 +33,6 @@ class Pipeline extends TypedList protected ?string $defaultGroup = null; - protected LoggerInterface $logger; - /** * @param list $pipes * @param list|null $groups Ordered group names/enums. When set, process() executes pipes in group order. @@ -71,7 +77,7 @@ public function walk(callable $walker): static public function process(mixed $payload) { foreach ($this->ordered() as $pipe) { - if ($pipe instanceof LoggerAwareInterface) { + if ($pipe instanceof LoggerAwareInterface && $this->logger instanceof LoggerInterface) { $pipe->setLogger($this->logger); } $payload = $pipe($payload) ?? $payload; @@ -149,7 +155,7 @@ public function configure(array $config): void if (method_exists($pipe, $setter)) { $pipe->{$setter}($value); } else { - $this->logger->warning("Unknown config option '{$pipeKey}.{$name}'"); + $this->logger?->warning("Unknown config option '{$pipeKey}.{$name}'"); } } }; @@ -158,7 +164,7 @@ public function configure(array $config): void foreach (array_keys($config) as $pipeKey) { if (!isset($applied[$pipeKey])) { - $this->logger->warning("Unknown config key '{$pipeKey}'; no matching pipe in this pipeline"); + $this->logger?->warning("Unknown config key '{$pipeKey}'; no matching pipe in this pipeline"); } } } diff --git a/upload/api/vendor/zircote/swagger-php/src/Utils/SecurityRequirement.php b/upload/api/vendor/zircote/swagger-php/src/Utils/SecurityRequirement.php new file mode 100644 index 00000000..5a28faf4 --- /dev/null +++ b/upload/api/vendor/zircote/swagger-php/src/Utils/SecurityRequirement.php @@ -0,0 +1,36 @@ + scopes map the OpenAPI specification defines. + * + * A requirement may also be written as a list of scheme names, `['bearerAuth']`, meaning those + * schemes with no scopes. Classic serialises its annotations, while hybrid and spec compile + * `Security\Requirement`, so this lives here for all three to share. + */ +final class SecurityRequirement +{ + /** + * @param array $requirement + * + * @return array + */ + public static function normalise(array $requirement): array + { + $normalised = []; + foreach ($requirement as $key => $value) { + if (is_int($key) && is_string($value)) { + $normalised[$value] = []; + } else { + $normalised[$key] = $value; + } + } + + return $normalised; + } +} diff --git a/upload/api/vendor/zircote/swagger-php/src/Utils/SourceLocation.php b/upload/api/vendor/zircote/swagger-php/src/Utils/SourceLocation.php index e463e4db..6d5acbf1 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Utils/SourceLocation.php +++ b/upload/api/vendor/zircote/swagger-php/src/Utils/SourceLocation.php @@ -57,6 +57,20 @@ public function __toString(): string return $location ?: 'unknown'; } + /** + * The method this location is in as `Class::method`, or null outside a method. + * + * The class is the one declaring the method, so a method a subclass inherits names its + * parent. That makes the string the same wherever the method is reached from, which is what + * an operation id or a route pairing needs from it. + */ + public function qualifiedMethod(): ?string + { + return $this->class !== null && $this->method !== null + ? $this->class . '::' . $this->method + : null; + } + public static function fromReflector(\Reflector $reflector): self { $class = null; diff --git a/upload/api/vendor/zircote/swagger-php/src/Utils/TokenScanner.php b/upload/api/vendor/zircote/swagger-php/src/Utils/TokenScanner.php index f855bd0b..8e25411a 100644 --- a/upload/api/vendor/zircote/swagger-php/src/Utils/TokenScanner.php +++ b/upload/api/vendor/zircote/swagger-php/src/Utils/TokenScanner.php @@ -50,7 +50,7 @@ public function scanFile(string $filename): array return $this->cache[$filename]; } - $code = file_get_contents($filename); + $code = is_file($filename) && is_readable($filename) ? file_get_contents($filename) : false; if (false === $code) { throw new \RuntimeException("Unable to read {$filename}"); } @@ -89,7 +89,8 @@ public function scanFile(string $filename): array public function detailsFor(\ReflectionClass $class): ?array { $filename = $class->getFileName(); - if ($filename === false) { + // internal classes have no file, and a class from `eval()` names one that does not exist + if ($filename === false || !is_file($filename)) { return null; }